reader/android: create/join syncgroup and watch for changes
Basic synchronization between two Android devices is working.
A dummy device set added from one device shows up on the other device.
subitems:
- bootstrap local/cloud syncbase instances
- create a syncgroup, if one does not exist
- join the syncgroup
- use watch API to react to data changes
- "Add Device Set" button for testing
Change-Id: I1aa650d6fc171eb2bb00975c2a78a9e2653aa14c
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 14c040d..d52a796 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
@@ -12,8 +12,15 @@
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.
@@ -25,6 +32,7 @@
private RecyclerView mRecyclerView;
private DeviceSetListAdapter mAdapter;
+ private Button mButtonAddDeviceSet;
private DB mDB;
protected void onCreate(Bundle savedInstanceState) {
@@ -42,6 +50,20 @@
// Use the linear layout manager for the recycler view
RecyclerView.LayoutManager layoutManager= new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(layoutManager);
+
+ mButtonAddDeviceSet = (Button) 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);
+ }
+ });
}
@Override
diff --git a/android/app/src/main/java/io/v/android/apps/reader/DeviceSetListAdapter.java b/android/app/src/main/java/io/v/android/apps/reader/DeviceSetListAdapter.java
index 9002e93..a43b65a 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/DeviceSetListAdapter.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/DeviceSetListAdapter.java
@@ -26,16 +26,19 @@
private OnDeviceSetClickListener mClickListener;
private DB mDB;
+ private DBList<File> mFiles;
private DBList<DeviceSet> mDeviceSets;
public class ViewHolder extends RecyclerView.ViewHolder {
public CardView mCardView;
- public TextView mTextView;
+ public TextView mTextViewTitle;
+ public TextView mTextViewId;
public ViewHolder(CardView v) {
super(v);
mCardView = v;
- mTextView = (TextView) mCardView.findViewById(R.id.device_set_list_item_text);
+ mTextViewTitle = (TextView) mCardView.findViewById(R.id.device_set_list_item_title);
+ mTextViewId = (TextView) mCardView.findViewById(R.id.device_set_list_item_id);
mCardView.setOnClickListener(new View.OnClickListener() {
@Override
@@ -52,6 +55,7 @@
mClickListener = null;
mDB = DB.Singleton.get(context);
+ mFiles = mDB.getFileList();
mDeviceSets = mDB.getDeviceSetList();
mDeviceSets.setListener(this);
}
@@ -67,17 +71,23 @@
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
- holder.mTextView.setText(getItemTitle(position));
+ DeviceSet ds = mDeviceSets.getItem(position);
+
+ holder.mTextViewTitle.setText("Title: " + getItemTitle(ds));
+ holder.mTextViewId.setText("Id: " + ds.getId());
}
public String getItemTitle(int position) {
- DeviceSet ds = mDeviceSets.getItem(position);
- File file = mDB.getFileById(ds.getFileId());
+ return getItemTitle(mDeviceSets.getItem(position));
+ }
+
+ public String getItemTitle(DeviceSet ds) {
+ File file = mFiles.getItemById(ds.getFileId());
if (file != null) {
return file.getTitle();
} else {
- return "*** Error retrieving the file name";
+ return "*** Error retrieving the title";
}
}
@@ -91,6 +101,8 @@
}
public void stop() {
+ mFiles.discard();
+ mFiles = null;
mDeviceSets.discard();
mDeviceSets = null;
}
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 38a2e37..abfd8e1 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
@@ -30,8 +30,8 @@
result = instance;
if (result == null) {
// uncomment either one
- instance = result = new FakeDB(context);
-// instance = result = new SyncbaseDB(context);
+// instance = result = new FakeDB(context);
+ instance = result = new SyncbaseDB(context);
}
}
}
@@ -72,6 +72,11 @@
E getItem(int position);
/**
+ * Returns the element with the given id.
+ */
+ E getItemById(String id);
+
+ /**
* Sets the listener for changes to the list.
* There can only be one listener.
*/
@@ -103,10 +108,9 @@
DBList<DeviceSet> getDeviceSetList();
/**
- * Gets the PDF file with the given id.
- * @param id the id of the file
- * @return the file object, or null if there is no file with the given id.
+ * Adds a new device set to the db.
+ * @param ds the device set to be added.
*/
- File getFileById(String id);
+ void addDeviceSet(DeviceSet ds);
}
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 4ca5244..008ffed 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
@@ -45,12 +45,10 @@
static abstract class BaseFakeList<E> implements DBList<E> {
- private Listener mListener;
+ protected Listener mListener;
@Override
public void setListener(Listener listener) {
- // This fake list never calls the notify methods.
- // Just check if the listener is set only once.
assert mListener == null;
mListener = listener;
}
@@ -92,6 +90,17 @@
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> {
@@ -114,6 +123,16 @@
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> {
@@ -144,6 +163,24 @@
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);
+ if (mListener != null) {
+ mListener.notifyItemInserted(mDeviceSets.size() - 1);
+ }
+ }
}
public void init(Activity activity) {
@@ -172,15 +209,7 @@
}
@Override
- public File getFileById(String id) {
- for (int i = 0; i < mFileList.getItemCount(); ++i) {
- File file = mFileList.getItem(i);
- if (file.getId().equals(id)) {
- return file;
- }
- }
-
- return null;
+ public void addDeviceSet(DeviceSet ds) {
+ mDeviceSetList.addItem(ds);
}
-
}
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 1f9b636..9ece043 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
@@ -7,12 +7,20 @@
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+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.Listener;
import io.v.android.apps.reader.vdl.Device;
import io.v.android.apps.reader.vdl.DeviceSet;
@@ -21,21 +29,36 @@
import io.v.android.v23.V;
import io.v.android.v23.services.blessing.BlessingCreationException;
import io.v.android.v23.services.blessing.BlessingService;
+import io.v.impl.google.naming.NamingUtil;
import io.v.impl.google.services.syncbase.SyncbaseServer;
+import io.v.v23.VIterable;
+import io.v.v23.context.CancelableVContext;
import io.v.v23.context.VContext;
import io.v.v23.rpc.Server;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.Blessings;
+import io.v.v23.security.VCertificate;
import io.v.v23.security.VPrincipal;
import io.v.v23.security.VSecurity;
import io.v.v23.security.access.AccessList;
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.KeyValue;
+import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
+import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
+import io.v.v23.services.syncbase.nosql.TableRow;
+import io.v.v23.services.watch.ResumeMarker;
import io.v.v23.syncbase.Syncbase;
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.Database;
+import io.v.v23.syncbase.nosql.RowRange;
+import io.v.v23.syncbase.nosql.Syncgroup;
import io.v.v23.syncbase.nosql.Table;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.verror.Errors;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
@@ -51,20 +74,22 @@
* The value must not conflict with any other blessing result codes.
*/
private static final int BLESSING_REQUEST = 200;
+
+ private static final String GLOBAL_MOUNT_TABLE = "/ns.dev.v.io:8101";
private static final String SYNCBASE_APP = "reader";
private static final String SYNCBASE_DB = "db";
-
- private static final String FILES_TABLE = "files";
- private static final String DEVICES_TABLE = "devices";
- private static final String DEVICE_SETS_TABLE = "deviceSets";
+ private static final String TABLE_FILES = "files";
+ private static final String TABLE_DEVICES = "devices";
+ private static final String TABLE_DEVICE_SETS = "deviceSets";
private Permissions mPermissions;
private Context mContext;
- private VContext vContext;
+ private VContext mVContext;
+ private SyncbaseHierarchy mLocalSB;
+ private boolean mInitialized;
- private Table mFiles;
- private Table mDevices;
- private Table mDeviceSets;
+ private String mUsername;
+ private String mSyncgroupName;
SyncbaseDB(Context context) {
mContext = context;
@@ -72,15 +97,15 @@
@Override
public void init(Activity activity) {
- if (vContext != null) {
+ if (mVContext != null) {
// Already initialized.
return;
}
- vContext = V.init(mContext);
+ mVContext = V.init(mContext);
try {
- vContext = V.withListenSpec(
- vContext, V.getListenSpec(vContext).withProxy("proxy"));
+ mVContext = V.withListenSpec(
+ mVContext, V.getListenSpec(mVContext).withProxy("proxy"));
} catch (VException e) {
handleError("Couldn't setup vanadium proxy: " + e.getMessage());
}
@@ -140,27 +165,42 @@
private void configurePrincipal(Blessings blessings) {
try {
- VPrincipal p = V.getPrincipal(vContext);
+ VPrincipal p = V.getPrincipal(mVContext);
p.blessingStore().setDefaultBlessings(blessings);
p.blessingStore().set(blessings, new BlessingPattern("..."));
VSecurity.addToRoots(p, blessings);
+
+ // "<user_email>/android"
+ mUsername = mountNameFromBlessings(blessings);
} catch (VException e) {
handleError(String.format(
"Couldn't set local blessing %s: %s", blessings, e.getMessage()));
return;
}
- setupSyncbase(blessings);
+
+ setupLocalSyncbase();
}
- private void setupSyncbase(Blessings blessings) {
+ private void setupLocalSyncbase() {
+ // "users/<user_email>/android/reader/<device_id>/syncbase"
+ final String syncbaseName = NamingUtil.join(
+ "users",
+ mUsername,
+ "reader",
+ DeviceInfoFactory.get(mContext).getId(),
+ "syncbase"
+ );
+ Log.i(TAG, "SyncbaseName: " + syncbaseName);
+
// Prepare the syncbase storage directory.
java.io.File storageDir = new java.io.File(mContext.getFilesDir(), "syncbase");
storageDir.mkdirs();
try {
- vContext = SyncbaseServer.withNewServer(
- vContext,
+ mVContext = SyncbaseServer.withNewServer(
+ mVContext,
new SyncbaseServer.Params()
+ .withName(syncbaseName)
.withPermissions(mPermissions)
.withStorageRootDir(storageDir.getAbsolutePath()));
} catch (SyncbaseServer.StartException e) {
@@ -169,41 +209,247 @@
}
try {
- Server syncbaseServer = V.getServer(vContext);
+ Server syncbaseServer = V.getServer(mVContext);
String serverName = "/" + syncbaseServer.getStatus().getEndpoints()[0];
- SyncbaseService service = Syncbase.newService(serverName);
- Toast.makeText(mContext, "serverName: " + serverName, Toast.LENGTH_LONG).show();
+ Log.i(TAG, "Local Syncbase ServerName: " + serverName);
- SyncbaseApp app = service.getApp(SYNCBASE_APP);
- if (!app.exists(vContext)) {
- app.create(vContext, mPermissions);
- }
+ mLocalSB = createHierarchy(serverName, "local");
- Database db = app.getNoSqlDatabase(SYNCBASE_DB, null);
- if (!db.exists(vContext)) {
- db.create(vContext, mPermissions);
- }
-
- mFiles = db.getTable(FILES_TABLE);
- if (!mFiles.exists(vContext)) {
- mFiles.create(vContext, mPermissions);
- }
-
- mDevices = db.getTable(DEVICES_TABLE);
- if (!mDevices.exists(vContext)) {
- mDevices.create(vContext, mPermissions);
- }
-
- mDeviceSets = db.getTable(DEVICE_SETS_TABLE);
- if (!mDeviceSets.exists(vContext)) {
- mDeviceSets.create(vContext, mPermissions);
- }
+ setupCloudSyncbase();
} catch (VException e) {
handleError("Couldn't setup syncbase service: " + e.getMessage());
}
}
+ /**
+ * This method assumes that there is a separate cloudsync instance running at:
+ * "users/[user_email]/reader/cloudsync"
+ */
+ private void setupCloudSyncbase() {
+ try {
+ // "users/<user_email>/reader/cloudsync"
+ String cloudsyncName = NamingUtil.join(
+ "users",
+ NamingUtil.trimSuffix(mUsername, "android"),
+ "reader/cloudsync"
+ );
+
+ SyncbaseHierarchy cloudSB = createHierarchy(cloudsyncName, "cloud");
+
+ createSyncgroup(cloudSB.db);
+ } catch (VException e) {
+ handleError("Couldn't setup cloudsync: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Creates a syncgroup at cloudsync with the following name:
+ * "users/[user_email]/reader/cloudsync/%%sync/cloudsync"
+ */
+ private void createSyncgroup(Database db) {
+ mSyncgroupName = NamingUtil.join(
+ "users",
+ NamingUtil.trimSuffix(mUsername, "android"),
+ "reader/cloudsync/%%sync/cloudsync"
+ );
+
+ Syncgroup group = db.getSyncgroup(mSyncgroupName);
+
+ List<TableRow> prefixes = ImmutableList.of(
+ new TableRow(TABLE_FILES, ""),
+ new TableRow(TABLE_DEVICES, ""),
+ new TableRow(TABLE_DEVICE_SETS, "")
+ );
+
+ List<String> mountTables = ImmutableList.of(
+ NamingUtil.join(
+ GLOBAL_MOUNT_TABLE,
+ "users",
+ NamingUtil.trimSuffix(mUsername, "android"),
+ "reader/rendezvous"
+ )
+ );
+
+ SyncgroupSpec spec = new SyncgroupSpec(
+ "reader syncgroup",
+ mPermissions,
+ prefixes,
+ mountTables,
+ false
+ );
+
+ try {
+ group.create(mVContext, spec, new SyncgroupMemberInfo((byte) 0));
+ Log.i(TAG, "Syncgroup is created successfully.");
+ } catch (VException e) {
+ if (e.is(Errors.EXIST)) {
+ Log.i(TAG, "Syncgroup already exists.");
+ } else {
+ handleError("Syncgroup could not be created: " + e.getMessage());
+ return;
+ }
+ }
+
+ joinSyncgroup();
+ }
+
+ /**
+ * Sets up the local syncbase to join the syncgroup.
+ */
+ private void joinSyncgroup() {
+ Syncgroup group = mLocalSB.db.getSyncgroup(mSyncgroupName);
+
+ try {
+ SyncgroupSpec spec = group.join(mVContext, new SyncgroupMemberInfo((byte) 0));
+ Log.i(TAG, "Successfully joined the syncgroup!");
+ Log.i(TAG, "Syncgroup spec: " + spec);
+
+ Map<String, SyncgroupMemberInfo> members = group.getMembers(mVContext);
+ for (String memberName : members.keySet()) {
+ Log.i(TAG, "Member: " + memberName);
+ }
+ } catch (VException e) {
+ handleError("Could not join the syncgroup: " + e.getMessage());
+ return;
+ }
+
+ mInitialized = true;
+
+ // When successfully joined the syncgroup, first register the device information.
+ registerDevice();
+ }
+
+ private void registerDevice() {
+ try {
+ Device thisDevice = DeviceInfoFactory.get(mContext);
+ mLocalSB.devices.put(mVContext, thisDevice.getId(), thisDevice, Device.class);
+ Log.i(TAG, "Registered this device to the syncbase table: " + thisDevice);
+ } catch (VException e) {
+ handleError("Could not register this device: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Creates the "[app]/[db]/[table]" hierarchy at the provided syncbase name.
+ */
+ private SyncbaseHierarchy createHierarchy(
+ String syncbaseName, String debugName) throws VException {
+
+ SyncbaseService service = Syncbase.newService(syncbaseName);
+
+ SyncbaseHierarchy result = new SyncbaseHierarchy();
+
+ result.app = service.getApp(SYNCBASE_APP);
+ if (!result.app.exists(mVContext)) {
+ result.app.create(mVContext, mPermissions);
+ Log.i(TAG, String.format(
+ "\"%s\" app is created at %s", result.app.name(), debugName));
+ } else {
+ Log.i(TAG, String.format(
+ "\"%s\" app already exists at %s", result.app.name(), debugName));
+ }
+
+ result.db = result.app.getNoSqlDatabase(SYNCBASE_DB, null);
+ if (!result.db.exists(mVContext)) {
+ result.db.create(mVContext, mPermissions);
+ Log.i(TAG, String.format(
+ "\"%s\" db is created at %s", result.db.name(), debugName));
+ } else {
+ Log.i(TAG, String.format(
+ "\"%s\" db already exists at %s", result.db.name(), debugName));
+ }
+
+ result.files = result.db.getTable(TABLE_FILES);
+ if (!result.files.exists(mVContext)) {
+ result.files.create(mVContext, mPermissions);
+ Log.i(TAG, String.format(
+ "\"%s\" table is created at %s", result.files.name(), debugName));
+ } else {
+ Log.i(TAG, String.format(
+ "\"%s\" table already exists at %s", result.files.name(), debugName));
+ }
+
+ result.devices = result.db.getTable(TABLE_DEVICES);
+ if (!result.devices.exists(mVContext)) {
+ result.devices.create(mVContext, mPermissions);
+ Log.i(TAG, String.format(
+ "\"%s\" table is created at %s", result.devices.name(), debugName));
+ } else {
+ Log.i(TAG, String.format(
+ "\"%s\" table already exists at %s", result.devices.name(), debugName));
+ }
+
+ result.deviceSets = result.db.getTable(TABLE_DEVICE_SETS);
+ if (!result.deviceSets.exists(mVContext)) {
+ result.deviceSets.create(mVContext, mPermissions);
+ Log.i(TAG, String.format(
+ "\"%s\" table is created at %s", result.deviceSets.name(), debugName));
+ } else {
+ Log.i(TAG, String.format(
+ "\"%s\" table already exists at %s", result.deviceSets.name(), debugName));
+ }
+
+ return result;
+ }
+
+ /**
+ * This method finds the last certificate in our blessing's certificate
+ * chains whose extension contains an '@'. We will assume that extension to
+ * represent our username.
+ */
+ private static String mountNameFromBlessings(Blessings blessings) {
+ for (List<VCertificate> chain : blessings.getCertificateChains()) {
+ for (VCertificate certificate : Lists.reverse(chain)) {
+ if (certificate.getExtension().contains("@")) {
+ return certificate.getExtension();
+ }
+ }
+ }
+ return "";
+ }
+
+ @Override
+ public DBList<File> getFileList() {
+ if (!mInitialized) {
+ return new EmptyList<>();
+ }
+
+ return new SyncbaseFileList(TABLE_FILES, File.class);
+ }
+
+ @Override
+ public DBList<Device> getDeviceList() {
+ if (!mInitialized) {
+ return new EmptyList<>();
+ }
+
+ return new SyncbaseDeviceList(TABLE_DEVICES, Device.class);
+ }
+
+ @Override
+ public DBList<DeviceSet> getDeviceSetList() {
+ if (!mInitialized) {
+ return new EmptyList<>();
+ }
+
+ return new SyncbaseDeviceSetList(TABLE_DEVICE_SETS, DeviceSet.class);
+ }
+
+ @Override
+ public void addDeviceSet(DeviceSet ds) {
+ try {
+ mLocalSB.deviceSets.put(mVContext, ds.getId(), ds, DeviceSet.class);
+ } catch (VException e) {
+ handleError("Failed to add the device set(" + ds + "): " + e.getMessage());
+ }
+ }
+
+ private void handleError(String msg) {
+ Log.e(TAG, msg);
+ Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();
+ }
+
// TODO(youngseokyoon): Remove once the list is implemented properly.
private static class EmptyList<E> implements DBList<E> {
@Override
@@ -217,6 +463,11 @@
}
@Override
+ public E getItemById(String id) {
+ return null;
+ }
+
+ @Override
public void setListener(Listener listener) {
}
@@ -225,28 +476,258 @@
}
}
- @Override
- public DBList<File> getFileList() {
- return new EmptyList<File>();
+ private class SyncbaseFileList extends SyncbaseDBList<File> {
+
+ public SyncbaseFileList(String tableName, Class clazz) {
+ super(tableName, clazz);
+ }
+
+ @Override
+ protected String getId(File file) {
+ return file.getId();
+ }
}
- @Override
- public DBList<Device> getDeviceList() {
- return new EmptyList<Device>();
+ private class SyncbaseDeviceList extends SyncbaseDBList<Device> {
+
+ public SyncbaseDeviceList(String tableName, Class clazz) {
+ super(tableName, clazz);
+ }
+
+ @Override
+ protected String getId(Device device) {
+ return device.getId();
+ }
}
- @Override
- public DBList<DeviceSet> getDeviceSetList() {
- return new EmptyList<DeviceSet>();
+ private class SyncbaseDeviceSetList extends SyncbaseDBList<DeviceSet> {
+
+ public SyncbaseDeviceSetList(String tableName, Class clazz) {
+ super(tableName, clazz);
+ }
+
+ @Override
+ protected String getId(DeviceSet deviceSet) {
+ return deviceSet.getId();
+ }
}
- @Override
- public File getFileById(String id) {
- return null;
+ private abstract class SyncbaseDBList<E> implements DBList<E> {
+
+ private final String TAG;
+
+ private CancelableVContext mCancelableVContext;
+ private Handler mHandler;
+ private Listener mListener;
+ private ResumeMarker mResumeMarker;
+ private String mTableName;
+ private Class mClass;
+ private List<E> mItems;
+
+ public SyncbaseDBList(String tableName, Class clazz) {
+ mCancelableVContext = mVContext.withCancel();
+ mTableName = tableName;
+ mClass = clazz;
+ mItems = new ArrayList<>();
+ mHandler = new Handler(Looper.getMainLooper());
+
+ TAG = String.format("%s<%s>",
+ SyncbaseDBList.class.getSimpleName(), mClass.getSimpleName());
+
+ readInitialData();
+
+ // Run this in a background thread
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ watchForChanges();
+ }
+ }).start();
+ }
+
+ private void readInitialData() {
+ try {
+ Log.i(TAG, "Reading initial data from table: " + mTableName);
+
+ BatchDatabase batch = mLocalSB.db.beginBatch(
+ mCancelableVContext, new BatchOptions("fetch", true));
+
+ // Read existing data from the table.
+ Table table = batch.getTable(mTableName);
+ VIterable<KeyValue> kvs = table.scan(mCancelableVContext, RowRange.range("", ""));
+ for (KeyValue kv : kvs) {
+ @SuppressWarnings("unchecked")
+ E item = (E) VomUtil.decode(kv.getValue(), mClass);
+ mItems.add(item);
+ }
+
+ // Remember this resume marker for the watch call.
+ mResumeMarker = batch.getResumeMarker(mVContext);
+
+ batch.abort(mCancelableVContext);
+
+ Log.i(TAG, "Done reading initial data from table: " + mTableName);
+ } catch (Exception e) {
+ handleError(e.getMessage());
+ }
+ }
+
+ private void watchForChanges() {
+ try {
+ // Watch for new changes coming from other Syncbase peers.
+ VIterable<WatchChange> watchStream = mLocalSB.db.watch(
+ mCancelableVContext, mTableName, "", mResumeMarker);
+
+ Log.i(TAG, "Watching for changes of table: " + mTableName + "...");
+
+ for (final WatchChange wc : watchStream) {
+ printWatchChange(wc);
+
+ // Handle the watch change differently, depending on the change type.
+ switch (wc.getChangeType()) {
+ case PUT_CHANGE:
+ // Run this in the UI thread.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ handlePutChange(wc);
+ }
+ });
+ break;
+
+ case DELETE_CHANGE:
+ // Run this in the UI thread.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ handleDeleteChange(wc);
+ }
+ });
+ break;
+ }
+ }
+ } catch (Exception e) {
+ handleError(e.getMessage());
+ Log.e(TAG, "Stack Trace: ", e);
+ }
+ }
+
+ private void printWatchChange(WatchChange wc) {
+ Log.i(TAG, "*** New Watch Change ***");
+ Log.i(TAG, "- ChangeType: " + wc.getChangeType().toString());
+ Log.i(TAG, "- RowName: " + wc.getRowName());
+ Log.i(TAG, "- TableName: " + wc.getTableName());
+ Log.i(TAG, "- VomValue: " + VomUtil.bytesToHexString(wc.getVomValue()));
+ Log.i(TAG, "- isContinued: " + wc.isContinued());
+ Log.i(TAG, "- isFromSync: " + wc.isFromSync());
+ Log.i(TAG, "========================");
+ }
+
+ private void handlePutChange(WatchChange wc) {
+ E item = null;
+
+ try {
+ item = (E) VomUtil.decode(wc.getVomValue(), mClass);
+ } catch (VException e) {
+ handleError("Could not decode the Vom: " + e.getMessage());
+ }
+
+ if (item == null) {
+ return;
+ }
+
+ boolean handled = false;
+ for (int i = 0; i < mItems.size(); ++i) {
+ E e = mItems.get(i);
+
+ if (wc.getRowName().equals(getId(e))) {
+ // Update the file record here.
+
+ mItems.remove(i);
+ mItems.add(i, item);
+
+ if (mListener != null) {
+ mListener.notifyItemChanged(i);
+ }
+
+ handled = true;
+ }
+ }
+
+ if (handled) {
+ return;
+ }
+
+ // This is a new row added in the table.
+ mItems.add(item);
+
+ if (mListener != null) {
+ mListener.notifyItemInserted(mItems.size() - 1);
+ }
+ }
+
+ private void handleDeleteChange(WatchChange wc) {
+ boolean handled = false;
+ for (int i = 0; i < mItems.size(); ++i) {
+ E e = mItems.get(i);
+
+ if (wc.getRowName().equals(getId(e))) {
+ mItems.remove(i);
+
+ if (mListener != null) {
+ mListener.notifyItemRemoved(i);
+ }
+
+ handled = true;
+ }
+ }
+
+ if (!handled) {
+ handleError("DELETE_CHANGE arrived but no matching item found in the table.");
+ }
+ }
+
+ 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 e : mItems) {
+ if (getId(e).equals(id)) {
+ return e;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ assert mListener == null;
+ mListener = listener;
+ }
+
+ @Override
+ public void discard() {
+ Log.i(TAG, "Cancelling the watch stream.");
+ mCancelableVContext.cancel();
+ }
}
- private void handleError(String msg) {
- Log.e(TAG, msg);
- Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();
+ private static class SyncbaseHierarchy {
+ public SyncbaseApp app;
+ public Database db;
+ public Table files;
+ public Table devices;
+ public Table deviceSets;
}
}
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 4a18c57..38e1971 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,13 +1,26 @@
-<android.support.v7.widget.RecyclerView
- xmlns:android="http://schemas.android.com/apk/res/android"
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/device_set_list"
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"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".DeviceSetChooserActivity">
-</android.support.v7.widget.RecyclerView>
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/device_set_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <Button
+ android:id="@+id/button_add_device_set"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_device_set" />
+
+</LinearLayout>
diff --git a/android/app/src/main/res/layout/device_set_list_item.xml b/android/app/src/main/res/layout/device_set_list_item.xml
index 851c899..7a70c23 100644
--- a/android/app/src/main/res/layout/device_set_list_item.xml
+++ b/android/app/src/main/res/layout/device_set_list_item.xml
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
+
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/device_set_list_item"
android:layout_width="match_parent"
@@ -9,13 +10,25 @@
android:layout_marginTop="@dimen/device_set_list_vertical_margin"
android:scrollbars="vertical">
- <TextView
- android:id="@+id/device_set_list_item_text"
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/device_set_list_item_vertical_margin"
android:layout_marginLeft="@dimen/device_set_list_item_horizontal_margin"
android:layout_marginRight="@dimen/device_set_list_item_horizontal_margin"
- android:layout_marginTop="@dimen/device_set_list_item_vertical_margin"/>
+ android:layout_marginTop="@dimen/device_set_list_item_vertical_margin"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/device_set_list_item_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:id="@+id/device_set_list_item_id"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
</android.support.v7.widget.CardView>
diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml
index 5fd0d9c..cc388d5 100644
--- a/android/app/src/main/res/values-v21/styles.xml
+++ b/android/app/src/main/res/values-v21/styles.xml
@@ -1,10 +1,14 @@
<?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/strings.xml b/android/app/src/main/res/values/strings.xml
index d043fa0..be1cf81 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,4 +1,5 @@
<resources>
<string name="app_name">PDF Reader</string>
<string name="action_settings">Settings</string>
+ <string name="add_device_set">Add Device Set</string>
</resources>