Merge branch 'master' into ble
diff --git a/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java b/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
index 928d625..ef85a61 100644
--- a/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
+++ b/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
@@ -4,8 +4,6 @@
package io.v.impl.google.rpc;
-import io.v.impl.google.channel.ChannelIterable;
-import io.v.v23.rpc.NetworkChange;
import io.v.v23.rpc.Server;
import io.v.v23.rpc.ServerStatus;
import io.v.v23.verror.VException;
@@ -16,9 +14,6 @@
private native void nativeAddName(long nativePtr, String name) throws VException;
private native void nativeRemoveName(long nativePtr, String name);
private native ServerStatus nativeGetStatus(long nativePtr) throws VException;
- private native Iterable<NetworkChange> nativeWatchNetwork(long nativePtr) throws VException;
- private native void nativeUnwatchNetwork(long nativePtr, ChannelIterable<NetworkChange> channel)
- throws VException;
private native void nativeStop(long nativePtr) throws VException;
private native void nativeFinalize(long nativePtr);
@@ -43,25 +38,6 @@
}
}
@Override
- public Iterable<NetworkChange> watchNetwork() {
- try {
- return nativeWatchNetwork(this.nativePtr);
- } catch (VException e) {
- throw new RuntimeException("Couldn't watch network", e);
- }
- }
- @Override
- public void unwatchNetwork(Iterable<NetworkChange> it) {
- if (it == null || !(it instanceof ChannelIterable)) {
- return;
- }
- try {
- nativeUnwatchNetwork(this.nativePtr, (ChannelIterable<NetworkChange>) it);
- } catch (VException e) {
- throw new RuntimeException("Couldn't unwatch network", e);
- }
- }
- @Override
public void stop() throws VException {
nativeStop(this.nativePtr);
}
diff --git a/lib/src/main/java/io/v/v23/rpc/Server.java b/lib/src/main/java/io/v/v23/rpc/Server.java
index 8bdbd81..0146525 100644
--- a/lib/src/main/java/io/v/v23/rpc/Server.java
+++ b/lib/src/main/java/io/v/v23/rpc/Server.java
@@ -34,26 +34,6 @@
ServerStatus getStatus();
/**
- * Returns an iterator over server's network changes. The returned iterator blocks
- * if there aren't any immediate changes. Some change events may be lost if the reader is
- * too slow in its iterations.
- * <p>
- * You should be aware that the iterator:
- * <p><ul>
- * <li>can be created <strong>only</strong> once</li>
- * <li>does not support {@link java.util.Iterator#remove remove}</li>
- * </ul>
- */
- Iterable<NetworkChange> watchNetwork();
-
- /**
- * Unregisters an iterator previously returned via {@link #watchNetwork}.
- *
- * @param it an iterator previously returned via {@link #watchNetwork}
- */
- void unwatchNetwork(Iterable<NetworkChange> it);
-
- /**
* Gracefully stops all services on this server. New calls are rejected, but any in-flight
* calls are allowed to complete. All published mountpoints are unmounted. This call waits for
* this process to complete and returns once the server has been shut down.
diff --git a/lib/src/test/java/io/v/v23/syncbase/SyncbaseTest.java b/lib/src/test/java/io/v/v23/syncbase/SyncbaseTest.java
index f92c383..9da28a8 100644
--- a/lib/src/test/java/io/v/v23/syncbase/SyncbaseTest.java
+++ b/lib/src/test/java/io/v/v23/syncbase/SyncbaseTest.java
@@ -17,7 +17,7 @@
import io.v.v23.services.syncbase.nosql.BlobRef;
import io.v.v23.services.syncbase.nosql.KeyValue;
import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
-import io.v.v23.services.syncbase.nosql.SyncgroupPrefix;
+import io.v.v23.services.syncbase.nosql.TableRow;
import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
import io.v.v23.syncbase.nosql.BatchDatabase;
import io.v.v23.syncbase.nosql.BlobReader;
@@ -317,7 +317,7 @@
// "A" creates the group.
SyncgroupSpec spec = new SyncgroupSpec("test", allowAll,
- ImmutableList.of(new SyncgroupPrefix(TABLE_NAME, "")),
+ ImmutableList.of(new TableRow(TABLE_NAME, "")),
ImmutableList.<String>of(), false);
SyncgroupMemberInfo memberInfo = new SyncgroupMemberInfo((byte) 1);
Syncgroup group = db.getSyncgroup(groupName);
@@ -331,14 +331,14 @@
// TODO(spetrovic): test leave() and destroy().
SyncgroupSpec specRMW = new SyncgroupSpec("testRMW", allowAll,
- ImmutableList.of(new SyncgroupPrefix(TABLE_NAME, "")),
+ ImmutableList.of(new TableRow(TABLE_NAME, "")),
ImmutableList.<String>of(), false);
assertThat(group.getSpec(ctx).keySet()).isNotEmpty();
String version = group.getSpec(ctx).keySet().iterator().next();
group.setSpec(ctx, specRMW, version);
assertThat(group.getSpec(ctx).values()).containsExactly(specRMW);
SyncgroupSpec specOverwrite = new SyncgroupSpec("testOverwrite", allowAll,
- ImmutableList.of(new SyncgroupPrefix(TABLE_NAME, "")),
+ ImmutableList.of(new TableRow(TABLE_NAME, "")),
ImmutableList.<String>of(), false);
group.setSpec(ctx, specOverwrite, "");
assertThat(group.getSpec(ctx).values()).containsExactly(specOverwrite);
diff --git a/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java b/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
index cd62a0c..20eaffe 100644
--- a/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
+++ b/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
@@ -17,7 +17,6 @@
import io.v.v23.rpc.Dispatcher;
import io.v.v23.rpc.Invoker;
import io.v.v23.rpc.ListenSpec;
-import io.v.v23.rpc.NetworkChange;
import io.v.v23.rpc.Server;
import io.v.v23.rpc.ServerCall;
import io.v.v23.rpc.ServiceObjectWithAuthorizer;
@@ -243,17 +242,6 @@
}
}
- public void testWatchNetwork() throws Exception {
- FortuneServer server = new FortuneServerImpl();
- ctx = V.withNewServer(ctx, "", server, null);
-
- // TODO(spetrovic): Figure out how to force network change in android and test that the
- // changes get announced on this channel.
- Server s = V.getServer(ctx);
- Iterable<NetworkChange> channel = s.watchNetwork();
- s.unwatchNetwork(channel);
- }
-
public void testContext() throws Exception {
FortuneServer server = new FortuneServerImpl();
ctx = V.withNewServer(ctx, "", server, null);
diff --git a/projects/syncslidepresenter/build.gradle b/projects/syncslidepresenter/build.gradle
index e13c1f1..258e6da 100644
--- a/projects/syncslidepresenter/build.gradle
+++ b/projects/syncslidepresenter/build.gradle
@@ -13,7 +13,10 @@
}
}
-mainClassName = "io.v.syncslidepresenter"
+mainClassName = "io.v.syncslidepresenter.Main"
+// Work around potential freezes in Java/Vanadium binaries
+// described by http://v.io/i/567.
+applicationDefaultJvmArgs = ['-XX:+UnlockDiagnosticVMOptions', '-XX:-LogEvents']
repositories {
mavenCentral()
@@ -25,12 +28,14 @@
compile 'com.beust:jcommander:1.48'
}
-task copyLib(type: Copy) {
+task copyLib(type: Copy, dependsOn: project(':lib').tasks.'copyVanadiumLib') {
from([project(':lib').buildDir.getAbsolutePath(), 'libs', 'libv23.dylib'].join(File.separator))
from([project(':lib').buildDir.getAbsolutePath(), 'libs', 'libv23.so'].join(File.separator))
destinationDir = new File(['src', 'main', 'resources'].join(File.separator))
}
+tasks.'processResources'.dependsOn(copyLib)
+
clean {
delete 'src/main/resources/libv23.so'
delete 'src/main/resources/libv23.dylib'
@@ -40,5 +45,5 @@
inputPaths += [project(':projects:syncslides').projectDir.absolutePath + '/app/src/main/java']
}
-sourceCompatibility = '1.8'
-targetCompatibility = '1.8'
\ No newline at end of file
+sourceCompatibility = '1.7'
+targetCompatibility = '1.7'
diff --git a/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java b/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
index 4f23fa5..73ba305 100644
--- a/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
+++ b/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
@@ -7,6 +7,7 @@
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
+import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -21,11 +22,27 @@
import java.awt.Window;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
-import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
+import java.io.File;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Set;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.rmi.AlreadyBoundException;
+import java.rmi.Naming;
+import java.rmi.NotBoundException;
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.rmi.registry.LocateRegistry;
+import java.rmi.server.UnicastRemoteObject;
import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -36,11 +53,18 @@
import io.v.android.apps.syncslides.db.VCurrentSlide;
import io.v.android.apps.syncslides.db.VSlide;
+import io.v.android.apps.syncslides.discovery.ParticipantClient;
+import io.v.android.apps.syncslides.discovery.ParticipantClientFactory;
+import io.v.android.apps.syncslides.discovery.Presentation;
import io.v.impl.google.naming.NamingUtil;
import io.v.impl.google.services.syncbase.SyncbaseServer;
import io.v.v23.V;
import io.v.v23.context.VContext;
+import io.v.v23.namespace.Namespace;
import io.v.v23.naming.Endpoint;
+import io.v.v23.naming.GlobReply;
+import io.v.v23.naming.MountEntry;
+import io.v.v23.naming.MountedServer;
import io.v.v23.rpc.Server;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.access.AccessList;
@@ -62,7 +86,22 @@
import io.v.v23.vom.VomUtil;
/**
- * The entry point for syncslidepresenter.
+ * The entry point for syncslidepresenter. To run:
+ *
+ * <pre>
+ * cd $JIRI_ROOT/release/java
+ * ./gradlew :projects:syncslidepresenter:installDist
+ * ./projects/syncslidepresenter/build/install/syncslidepresenter/bin/syncslidepresenter
+ * </pre>
+ *
+ * <p>For reasons not yet fully understood, applications running both Swing and Vanadium freeze
+ * randomly (at least on Mac OS X). We work around this by running two JVMs. The main JVM will
+ * create a sub-JVM in a separate thread, and will then proceed to join a syncgroup and look for
+ * slide data. Once a new slide is detected, the bytes representing the slide will be sent to
+ * the sub-JVM via RMI.
+ *
+ * <p>The sub-JVM simply listens for slide bytes and then, using {@link ImageIO#read}, decodes
+ * those bytes and displays the resulting image in a JFrame.
*/
public class Main {
private static final Logger logger = Logger.getLogger(Main.class.getName());
@@ -70,6 +109,7 @@
private static final String SYNCBASE_DB = "syncslides";
private static final String PRESENTATIONS_TABLE = "Presentations";
private static final String DECKS_TABLE = "Decks";
+ private static final int MAX_PORT_PICKER_ATTEMPTS = 100;
private final Table presentations;
private final Table decks;
private final ImageViewer viewer;
@@ -78,7 +118,9 @@
private VContext context;
- public static void main(String[] args) throws SyncbaseServer.StartException, VException, IOException {
+ public static void main(String[] args)
+ throws SyncbaseServer.StartException, VException, IOException, NotBoundException,
+ AlreadyBoundException {
Options options = new Options();
JCommander commander = new JCommander(options);
try {
@@ -94,13 +136,16 @@
return;
}
- // Make command-Q do the same as closing the main frame (i.e. exit).
- System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
+ ImageViewer viewer = null;
+ if (options.swing) {
+ // We're just going to be running Swing. Do that and then stop.
+ startSwingServer();
+ return;
+ }
- JFrame frame = new JFrame();
- enableOSXFullscreen(frame);
- frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
- frame.setVisible(true);
+ // Otherwise, we're a Vanadium client. Connect to the swing server and then
+ // set up vanadium.
+ viewer = startAndConnectToSwingServer(options.swingServerTimeoutSeconds);
VContext baseContext = V.init();
@@ -109,10 +154,10 @@
Permissions permissions = new Permissions(ImmutableMap.of("1", acl));
String name = NamingUtil.join(options.mountPrefix, UUID.randomUUID().toString());
logger.info("Mounting new syncbase server at " + name);
- baseContext = SyncbaseServer.withNewServer(
- baseContext.withTimeout(Duration.standardSeconds(options.mountTimeoutSeconds)),
- new SyncbaseServer.Params().withPermissions(permissions).withName(name));
- final Server server = V.getServer(baseContext);
+ VContext mountContext = SyncbaseServer.withNewServer(baseContext,
+ new SyncbaseServer.Params().withPermissions(permissions).withName(name)
+ .withStorageRootDir(options.storageRootDir));
+ final Server server = V.getServer(mountContext);
if (server.getStatus().getEndpoints().length > 0) {
logger.info("Mounted syncbase server at the following endpoints: ");
for (Endpoint e : server.getStatus().getEndpoints()) {
@@ -139,22 +184,46 @@
presentations.create(baseContext, permissions);
}
- JPanel panel = new JPanel(new GridBagLayout());
- ScaleToFitJPanel presentationPanel = new ScaleToFitJPanel();
- GridBagConstraints constraints = new GridBagConstraints();
- constraints.weightx = 1;
- constraints.weighty = 1;
- constraints.fill = GridBagConstraints.BOTH;
- panel.add(presentationPanel, constraints);
- frame.getContentPane().add(panel);
- frame.pack();
+ Main m = new Main(baseContext, viewer, db, decks, presentations);
- Main m = new Main(baseContext, presentationPanel, db, decks, presentations);
- m.joinPresentation(options.presentationName, options.joinTimeoutSeconds,
- options.currentSlideKey, options.slideRowFormat);
+ Presentation presentation = new Discovery(
+ baseContext, options.mountPrefix, options.deckPrefix).getPresentation();
+ logger.info("Using presentation: " + presentation);
+ m.joinPresentation(presentation, options.joinTimeoutSeconds, options.slideRowFormat);
}
}
+ private static void startSwingServer() throws IOException, AlreadyBoundException {
+ logger.info("inside startSwingServer");
+ // Make command-Q do the same as closing the main frame (i.e. exit).
+ System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
+ System.setProperty("java.rmi.server.hostname", "localhost");
+
+ JFrame frame = new JFrame();
+ enableOSXFullscreen(frame);
+ frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ frame.setVisible(true);
+
+ JPanel panel = new JPanel(new GridBagLayout());
+ ScaleToFitJPanel presentationPanel = new ScaleToFitJPanel();
+ GridBagConstraints constraints = new GridBagConstraints();
+ constraints.weightx = 1;
+ constraints.weighty = 1;
+ constraints.fill = GridBagConstraints.BOTH;
+ panel.add(presentationPanel, constraints);
+ frame.getContentPane().add(panel);
+ frame.pack();
+
+ int port = pickUnusedPort();
+ LocateRegistry.createRegistry(port);
+ String name = "//localhost:" + port + "/" + UUID.randomUUID();
+ logger.info("swing server binding to " + name);
+ Naming.bind(name, new RemoteImageViewer(presentationPanel));
+
+ // The parent JVM will expect to read this value.
+ System.out.println(name);
+ }
+
public Main(VContext context, ImageViewer viewer, Database db, Table decks,
Table presentations) throws VException {
this.context = context;
@@ -164,11 +233,10 @@
this.viewer = viewer;
}
- public void joinPresentation(final String syncgroupName,
+ public void joinPresentation(final Presentation presentation,
int joinTimeoutSeconds,
- String currentSlideKey,
String slideRowFormat) throws VException {
- Syncgroup syncgroup = db.getSyncgroup(syncgroupName);
+ Syncgroup syncgroup = db.getSyncgroup(presentation.getSyncgroupName());
syncgroup.join(context.withTimeout(Duration.standardSeconds(joinTimeoutSeconds)),
new SyncgroupMemberInfo((byte) 1));
for (String member : syncgroup.getMembers(context).keySet()) {
@@ -180,7 +248,10 @@
}
BatchDatabase batch = db.beginBatch(context, null);
ResumeMarker marker = batch.getResumeMarker(context);
- Stream<WatchChange> watchStream = db.watch(context, presentations.name(), currentSlideKey,
+ String rowKey = Joiner.on("/").join(presentation.getDeckId(), presentation
+ .getPresentationId(), "CurrentSlide");
+ logger.info("going to watch row key " + rowKey);
+ Stream<WatchChange> watchStream = db.watch(context, presentations.name(), rowKey,
marker);
for (WatchChange w : watchStream) {
@@ -191,11 +262,10 @@
VCurrentSlide.class);
logger.info("Current slide: " + currentSlide);
// Read the corresponding slide.
- VSlide slide = (VSlide) decks.getRow(String.format(slideRowFormat,
- currentSlide.getNum())).get(context, VSlide.class);
- final BufferedImage image = ImageIO.read(
- new ByteArrayInputStream(slide.getThumbnail()));
- viewer.setImage(image);
+ String row = String.format(slideRowFormat, presentation.getDeckId(),
+ currentSlide.getNum());
+ VSlide slide = (VSlide) decks.getRow(row).get(context, VSlide.class);
+ viewer.setImage(slide.getThumbnail());
} catch (IOException | VException e) {
logger.log(Level.WARNING, "exception encountered while handling change event", e);
}
@@ -221,33 +291,36 @@
}
public static class Options {
+ @Parameter(names = {"-s", "--storageRootDir"},
+ description = "the root directory to use for local storage")
+ private String storageRootDir = Joiner.on(File.separator).join(
+ System.getProperty("java.io.tmpdir"), "syncslidepresenter-storage");
+
+ @Parameter(names = {"-d", "--deckPrefix"},
+ description = "mounttable prefix for live presentations.")
+ private String deckPrefix = "happyDeck";
+
+ @Parameter(names = {"--swing"})
+ private boolean swing = false;
+
@Parameter(names = {"-m", "--mountPrefix"}, description = "the base path in the namespace"
+ " where the syncbase service will be mounted")
private String mountPrefix = "/192.168.86.254:8101";
- @Parameter(names = {"-p", "--presentationName"},
- description = "the presentation to watch")
- private String presentationName = "/192.168.86.254:8101/990005300000537/%%sync/"
- + "syncslides/deckId1/randomPresentationId1";
-
- @Parameter(names = {"--mountTimeout"},
- description = "the number of seconds to wait for the syncbase server to be created")
- private int mountTimeoutSeconds = 10;
-
@Parameter(names = {"--joinTimeout"},
description = "the number of seconds to wait to join the presentation")
private int joinTimeoutSeconds = 10;
- @Parameter(names = {"-k", "--currentSlideKey"},
- description = "the row key containing the current slide")
- private String currentSlideKey = "deckId1/randomPresentationId1/CurrentSlide";
-
@Parameter(names = {"-f", "--slideRowFormat"},
description = "a pattern specifying where slide rows are found")
- private String slideRowFormat = "deckId1/slides/%04d";
+ private String slideRowFormat = "%s/slides/%04d";
@Parameter(names = {"-h", "--help"}, description = "display this help message", help = true)
private boolean help = false;
+
+ @Parameter(names = {"--swingServerTimeoutSeconds"},
+ description = "the amount of time to wait for the swing server to start")
+ private int swingServerTimeoutSeconds = 10;
}
private static class ScaleToFitJPanel extends JPanel implements ImageViewer {
@@ -291,14 +364,169 @@
}
@Override
- public void setImage(Image image) {
- this.image = image;
- setPreferredSize(new Dimension(image.getWidth(null), image.getHeight(null)));
- repaint();
+ public void setImage(byte[] imageData) throws RemoteException {
+ try {
+ this.image = ImageIO.read(new ByteArrayInputStream(imageData));
+ setPreferredSize(new Dimension(image.getWidth(null), image.getHeight(null)));
+ repaint();
+ } catch (IOException e) {
+ throw new RemoteException("Could not set image", e);
+ }
}
}
- private interface ImageViewer {
- void setImage(Image image);
+ private interface ImageViewer extends Remote {
+ void setImage(byte[] imageData) throws RemoteException;
+ }
+
+ private static class RemoteImageViewer extends UnicastRemoteObject implements ImageViewer {
+ private final ImageViewer viewer;
+
+ protected RemoteImageViewer(ImageViewer viewer) throws RemoteException {
+ super();
+
+ this.viewer = viewer;
+ }
+
+ @Override
+ public void setImage(byte[] imageData) throws RemoteException {
+ viewer.setImage(imageData);
+ }
+ }
+
+ private static ImageViewer startAndConnectToSwingServer(int timeoutSeconds) throws IOException,
+ NotBoundException {
+ String javaHome = System.getProperty("java.home");
+ if (javaHome == null) {
+ throw new IllegalStateException("System property java.home is not set");
+ }
+ File javaBin = new File(javaHome, Joiner.on(File.separator).join("bin", "java"));
+ if (!javaBin.exists() || !javaBin.canExecute()) {
+ throw new IllegalStateException("Java binary " + javaBin
+ + " does not exist or is not executable");
+ }
+ ProcessBuilder builder = new ProcessBuilder(javaBin.getAbsolutePath(),
+ "-classpath", System.getProperty("java.class.path"),
+ Main.class.getCanonicalName(), "--swing");
+ builder.redirectError(ProcessBuilder.Redirect.INHERIT);
+ logger.info("starting " + builder.command());
+ // The first line of output should be the address to which we should connect.
+ final Process process = builder.start();
+ logger.info("started " + process);
+
+ // Get a timeout timer going.
+ ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
+ service.schedule(new Runnable() {
+ @Override
+ public void run() {
+ process.destroyForcibly();
+ }
+ }, timeoutSeconds, TimeUnit.SECONDS);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+ String name = reader.readLine();
+ // If the timeout didn't fire, we don't want it killing our process now!
+ service.shutdownNow();
+ if (name != null) {
+ logger.info("vanadium listener sending slide images to " + name);
+ ImageViewer viewer = (ImageViewer) Naming.lookup(name);
+ // Hack: when the swing viewer exits, terminate our main app.
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ do {
+ try {
+ process.waitFor();
+ break;
+ } catch (InterruptedException e) {
+ // Ignored.
+ }
+ } while (true);
+ System.exit(0);
+ }
+ }, "SwingServerTerminationWatcher").start();
+ return viewer;
+ } else {
+ throw new IOException("could not read slide name");
+ }
+ }
+
+ private static int pickUnusedPort() throws IOException {
+ for (int i = 0; i < MAX_PORT_PICKER_ATTEMPTS; i++) {
+ ServerSocket socket = new ServerSocket();
+ try {
+ socket.bind(new InetSocketAddress(0));
+ int port = ((InetSocketAddress) socket.getLocalSocketAddress()).getPort();
+ return port;
+ } catch (IOException e) {
+ // Couldn't bind, try again.
+ } finally {
+ try {
+ socket.close();
+ } catch (IOException closeException) {
+ logger.log(Level.WARNING, "Exception caught during close", closeException);
+ }
+ }
+ }
+
+ throw new IOException("Couldn't locate unused port");
+ }
+
+ private static class Discovery {
+ public static final Duration MT_TIMEOUT =
+ Duration.standardSeconds(10);
+
+ private final VContext context;
+ private final String mtName;
+ private final String deckPrefix;
+
+ public Discovery(VContext context, String mtName, String deckPrefix) {
+ this.context = context;
+ this.mtName = mtName;
+ this.deckPrefix = deckPrefix;
+ }
+
+ private Set<String> scan(String pattern) {
+ logger.info("Scanning MT " + mtName + " with pattern \"" + pattern + "\"");
+ try {
+ Namespace ns = V.getNamespace(context);
+ ns.setRoots(ImmutableList.of(mtName));
+ Set<String> result = new HashSet<String>();
+ VContext ctx = context.withTimeout(MT_TIMEOUT);
+ for (GlobReply reply : V.getNamespace(ctx).glob(ctx, pattern)) {
+ if (reply instanceof GlobReply.Entry) {
+ MountEntry entry = ((GlobReply.Entry) reply).getElem();
+ result.add(entry.getName());
+ for (MountedServer server : entry.getServers()) {
+ logger.info(" endPoint: \"" + server.getServer() + "\"");
+ }
+ }
+ }
+ return result;
+ } catch (VException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private Presentation findPreso(String serviceName) {
+ V.getNamespace(context).flushCacheEntry(context, serviceName);
+ ParticipantClient client =
+ ParticipantClientFactory.getParticipantClient(serviceName);
+ try {
+ Presentation p = client.get(context.withTimeout(
+ Duration.standardSeconds(5)));
+ return p;
+ } catch (VException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public Presentation getPresentation() {
+ Set<String> services = scan(deckPrefix + "/*");
+ if (services.size() < 1) {
+ return null;
+ }
+ // Just grab the first one.
+ return findPreso(services.iterator().next());
+ }
}
}
diff --git a/projects/syncslides/app/src/main/AndroidManifest.xml b/projects/syncslides/app/src/main/AndroidManifest.xml
index 6030afe..0dff6f4 100644
--- a/projects/syncslides/app/src/main/AndroidManifest.xml
+++ b/projects/syncslides/app/src/main/AndroidManifest.xml
@@ -8,9 +8,13 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
+ <!-- Read the user's name from contacts so we can display it with the question. -->
+ <uses-permission android:name="android.permission.READ_PROFILE" />
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+
<application
android:allowBackup="true"
- android:icon="@mipmap/ic_launcher"
+ android:icon="@mipmap/vanadium_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserActivity.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserActivity.java
index 36304cb..c9a8d92 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserActivity.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserActivity.java
@@ -38,7 +38,6 @@
V23Manager.Singleton.get().init(getApplicationContext(), this);
mDB = DB.Singleton.get(getApplicationContext());
- mDB.init(this);
setContentView(R.layout.activity_deck_chooser);
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserFragment.java
index 6c25a93..0307bf4 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserFragment.java
@@ -229,9 +229,11 @@
Slide[] ret = new Slide[slides.length()];
for (int i = 0; i < slides.length(); ++i) {
JSONObject slide = slides.getJSONObject(i);
+ // TODO(jregan): Avoid the extra image conversion work.
+ // Reading into a bitmap, only to compress into bytes again.
Bitmap thumb = readImage(dir, slide.getString("Thumb"));
String note = slide.getString("Note");
- ret[i] = new SlideImpl(thumb, note);
+ ret[i] = new SlideImpl(DeckFactory.makeBytesFromBitmap(thumb), note);
}
return ret;
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java
index 75fbe0b..e46130d 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java
@@ -17,6 +17,9 @@
import android.widget.TextView;
import android.widget.Toolbar;
+import java.util.Calendar;
+import java.util.Locale;
+
import io.v.android.apps.syncslides.db.DB;
import io.v.android.apps.syncslides.discovery.DiscoveryManager;
import io.v.android.apps.syncslides.model.Deck;
@@ -32,7 +35,7 @@
implements Listener {
private static final String TAG = "DeckListAdapter";
private DB.DBList<Deck> mDecks;
- private DB.DBList<Deck> mLiveDecks;
+ private DiscoveryManager mLiveDecks;
private DB mDB;
public DeckListAdapter(DB db) {
@@ -87,7 +90,7 @@
}
@Override
- public void onBindViewHolder(final ViewHolder holder, int deckIndex) {
+ public void onBindViewHolder(final ViewHolder holder, final int deckIndex) {
final Deck deck;
final Role role;
// If the position is less than the number of live presentation decks, get deck card from
@@ -103,7 +106,10 @@
} else {
deck = mDecks.get(deckIndex - mLiveDecks.getItemCount());
// TODO(afergan): Set actual date here.
- holder.mToolbarLastOpened.setText("Opened on Oct 26, 2015");
+ final Calendar cal = Calendar.getInstance();
+ holder.mToolbarLastOpened.setText("Opened on "
+ + cal.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.US) + " "
+ + cal.get(Calendar.DAY_OF_MONTH) + ", " + cal.get(Calendar.YEAR));
holder.mToolbarLastOpened.setVisibility(View.VISIBLE);
holder.mToolbarLiveNow.setVisibility(View.GONE);
holder.mToolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@@ -132,28 +138,37 @@
Log.d(TAG, "Clicking through to PresentationActivity.");
final Context context = v.getContext();
if (role == Role.AUDIENCE) {
- //String deviceId = "355499060906851"; // Black Nexus 6.
- String deviceId = "355499060490393"; // Nexus 6 ZX1G22MLNL
+ final Participant p = mLiveDecks.getParticipant(deckIndex);
+ Log.d(TAG, "Joining:");
+ Log.d(TAG, " syncgroupName = " + p.getSyncgroupName());
+ Log.d(TAG, " presentationId = " + p.getPresentationId());
DB.Singleton.get(context).joinPresentation(
- // TODO(kash): Use the real syncgroup name.
- "/192.168.86.254:8101/" + deviceId + "/%%sync/syncslides/" +
- "deckId1/randomPresentationId1",
+ p.getSyncgroupName(),
new DB.Callback<Void>() {
@Override
public void done(Void aVoid) {
- showSlides(context, deck, role);
+ showSlides(
+ context, deck, role,
+ p.getSyncgroupName(),
+ p.getPresentationId());
}
});
} else {
- showSlides(context, deck, role);
+ showSlides(
+ context, deck, role,
+ Participant.Unknown.SYNCGROUP_NAME,
+ Participant.Unknown.PRESENTATION_ID);
}
}
});
}
- private void showSlides(Context context, Deck deck, Role role) {
+ private void showSlides(Context context, Deck deck, Role role,
+ String syncName, String presId) {
Intent intent = new Intent(context, PresentationActivity.class);
intent.putExtra(Deck.B.DECK_ID, deck.getId());
+ intent.putExtra(Participant.B.SYNCGROUP_NAME, syncName);
+ intent.putExtra(Participant.B.PRESENTATION_ID, presId);
intent.putExtra(Participant.B.PARTICIPANT_ROLE, role);
context.startActivity(intent);
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigateFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigateFragment.java
index 46dfc86..51a051c 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigateFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigateFragment.java
@@ -30,9 +30,12 @@
import java.util.List;
import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.db.VPerson;
+import io.v.android.apps.syncslides.misc.V23Manager;
import io.v.android.apps.syncslides.model.Question;
import io.v.android.apps.syncslides.model.Role;
import io.v.android.apps.syncslides.model.Slide;
+import io.v.v23.security.Blessings;
/**
* Provides both the presenter and audience views for navigating through a presentation.
@@ -76,9 +79,10 @@
private Role mRole;
private List<Question> mQuestionList;
private DB.QuestionListener mQuestionListener;
+ private boolean mDriving = false;
+ private DB.DriverListener mDriverListener;
private boolean mEditing;
- private int mQuestionerPosition;
- private boolean mSynced;
+ private String mQuestionId;
private DB.CurrentSlideListener mCurrentSlideListener;
private DB mDB;
private TextView mSlideNumText;
@@ -113,16 +117,11 @@
mDeckId = args.getString(DECK_ID_KEY);
mPresentationId = args.getString(PRESENTATION_ID_KEY);
mLoadingCurrentSlide = -1;
- mUserSlideNum = args.getInt(SLIDE_NUM_KEY);
+ mCurrentSlideNum = mUserSlideNum = args.getInt(SLIDE_NUM_KEY);
mRole = (Role) args.get(ROLE_KEY);
- if (((PresentationActivity) getActivity()).getSynced()) {
- sync();
- } else {
- unsync();
- }
final View rootView = inflater.inflate(R.layout.fragment_navigate, container, false);
mFabSync = rootView.findViewById(R.id.audience_sync_fab);
- if (mSynced || mRole != Role.AUDIENCE) {
+ if (((PresentationActivity) getActivity()).getSynced() || mRole != Role.AUDIENCE) {
mFabSync.setVisibility(View.INVISIBLE);
} else {
mFabSync.setVisibility(View.VISIBLE);
@@ -234,11 +233,15 @@
// the slides to load.
if (mLoadingCurrentSlide != -1) {
currentSlideChanged(mLoadingCurrentSlide);
- } else {
- updateView();
}
+ updateView();
}
});
+ if (((PresentationActivity) getActivity()).getSynced()) {
+ sync();
+ } else {
+ unsync();
+ }
return rootView;
}
@@ -247,14 +250,27 @@
public void onStart() {
super.onStart();
((PresentationActivity) getActivity()).setUiImmersive(true);
+ mCurrentSlideListener = new DB.CurrentSlideListener() {
+ @Override
+ public void onChange(int slideNum) {
+ NavigateFragment.this.currentSlideChanged(slideNum);
+ }
+ };
+ mDB.addCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener);
if (mRole == Role.AUDIENCE) {
- mCurrentSlideListener = new DB.CurrentSlideListener() {
+ final Blessings blessings = V23Manager.Singleton.get().getBlessings();
+ mDriverListener = new DB.DriverListener() {
@Override
- public void onChange(int slideNum) {
- NavigateFragment.this.currentSlideChanged(slideNum);
+ public void onChange(VPerson driver) {
+ if (driver != null && driver.getBlessing().equals(blessings.toString())) {
+ mDriving = true;
+ } else {
+ mDriving = false;
+ }
+
}
};
- mDB.addCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener);
+ mDB.setDriverListener(mDeckId, mPresentationId, mDriverListener);
}
if (mRole == Role.PRESENTER) {
mQuestionListener = new DB.QuestionListener() {
@@ -277,8 +293,9 @@
public void onStop() {
super.onStop();
((PresentationActivity) getActivity()).setUiImmersive(false);
+ mDB.removeCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener);
if (mRole == Role.AUDIENCE) {
- mDB.removeCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener);
+ mDB.removeDriverListener(mDeckId, mPresentationId, mDriverListener);
}
if (mRole == Role.PRESENTER) {
mDB.removeQuestionListener(mDeckId, mPresentationId, mQuestionListener);
@@ -312,30 +329,37 @@
return false;
}
+ /**
+ * If the user is editing the text field and the text has changed, save the
+ * notes locally and in Syncbase.
+ */
public void saveNotes() {
- if (mEditing) {
- Toast.makeText(getContext(), "Saving notes", Toast.LENGTH_SHORT).show();
- mNotes.clearFocus();
- InputMethodManager inputManager =
- (InputMethodManager) getContext().
- getSystemService(Context.INPUT_METHOD_SERVICE);
+ final String notes = mNotes.getText().toString();
+ if (mEditing && (!notes.equals(mSlides.get(mUserSlideNum).getNotes()))) {
+ toast("Saving notes");
+ mSlides.get(mUserSlideNum).setNotes(notes);
+ mDB.setSlideNotes(mDeckId, mUserSlideNum, notes);
+ }
+ mNotes.clearFocus();
+ InputMethodManager inputManager =
+ (InputMethodManager) getContext().
+ getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (getActivity().getCurrentFocus() != null) {
inputManager.hideSoftInputFromWindow(
getActivity().getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
- ((PresentationActivity) getActivity()).setUiImmersive(true);
}
+ ((PresentationActivity) getActivity()).setUiImmersive(true);
}
private void unsync() {
- if (mRole == Role.AUDIENCE && mSynced) {
- mSynced = false;
+ if (mRole == Role.AUDIENCE && ((PresentationActivity) getActivity()).getSynced()) {
((PresentationActivity) getActivity()).setUnsynced();
mFabSync.setVisibility(View.VISIBLE);
}
}
private void sync() {
- mSynced = true;
mUserSlideNum = mCurrentSlideNum;
((PresentationActivity) getActivity()).setSynced();
updateView();
@@ -351,7 +375,7 @@
}
if (mUserSlideNum < mSlides.size() - 1) {
mUserSlideNum++;
- if (mRole == Role.PRESENTER) {
+ if (mRole == Role.PRESENTER || mDriving) {
mDB.setCurrentSlide(mDeckId, mPresentationId, mUserSlideNum);
}
updateView();
@@ -369,7 +393,7 @@
}
if (mUserSlideNum > 0) {
mUserSlideNum--;
- if (mRole == Role.PRESENTER) {
+ if (mRole == Role.PRESENTER || mDriving) {
mDB.setCurrentSlide(mDeckId, mPresentationId, mUserSlideNum);
}
updateView();
@@ -388,7 +412,7 @@
return;
}
mCurrentSlideNum = slideNum;
- if (mSynced) {
+ if (((PresentationActivity) getActivity()).getSynced()) {
mUserSlideNum = slideNum;
updateView();
}
@@ -402,22 +426,15 @@
DB db = DB.Singleton.get(getActivity().getApplicationContext());
switch (mRole) {
case AUDIENCE:
- db.askQuestion(mDeckId, mPresentationId, "Audience member", "#1");
- Toast toast = Toast.makeText(getActivity().getApplicationContext(),
- "You have been added to the Q&A queue.", Toast.LENGTH_LONG);
- toast.show();
+ db.askQuestion(mDeckId, mPresentationId,
+ SignInActivity.getUserName(getActivity()));
+ toast("You have been added to the Q&A queue.");
break;
case PRESENTER:
if (mQuestionList == null || mQuestionList.size() == 0) {
break;
}
- // TODO(kash): It would be better to pass the entire Question to the dialog.
- String[] questioners = new String[mQuestionList.size()];
- for (int i = 0; i < mQuestionList.size(); i++) {
- questioners[i] = mQuestionList.get(i).getFirstName() + " "
- + mQuestionList.get(i).getLastName();
- }
- DialogFragment dialog = QuestionDialogFragment.newInstance(questioners);
+ DialogFragment dialog = QuestionDialogFragment.newInstance(mQuestionList);
dialog.setTargetFragment(this, DIALOG_REQUEST_CODE);
dialog.show(getFragmentManager(), "QuestionerDialogFragment");
break;
@@ -455,10 +472,7 @@
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DIALOG_REQUEST_CODE) {
- // TODO(afergan): Using the position is insufficient if the list of questioners changes
- // while the dialog is showing.
- mQuestionerPosition = data.getIntExtra(
- QuestionDialogFragment.QUESTION_BUNDLE_KEY, 0);
+ mQuestionId = data.getStringExtra(QuestionDialogFragment.QUESTION_ID_KEY);
handoffControl();
}
}
@@ -469,20 +483,33 @@
* text.
*/
private void handoffControl() {
- //TODO(afergan): Change slide presenter to the audience member at mQuestionerPosition.
+ Question handoff = null;
+ for (Question question : mQuestionList) {
+ if (question.getId().equals(mQuestionId)) {
+ handoff = question;
+ break;
+ }
+ }
+ if (handoff == null) {
+ toast("No such question");
+ return;
+ }
+
+ sync();
+ mDB.handoffQuestion(mDeckId, mPresentationId, handoff.getId());
+
View.OnClickListener snackbarClickListener = new NavigateClickListener() {
@Override
public void onClick(View v) {
super.onClick(v);
- //TODO(afergan): End handoff, presenter regains control of presentation.
+ mDB.resumeControl(mDeckId, mPresentationId);
}
};
((PresentationActivity) getActivity()).setUiImmersive(true);
- Snackbar snack = Snackbar.make(getView(), getResources().getString(
- R.string.handoff_message) + " "
- + mQuestionList.get(mQuestionerPosition).getFirstName() + " "
- + mQuestionList.get(mQuestionerPosition).getLastName(),
+ Snackbar snack = Snackbar.make(
+ getView(),
+ getResources().getString(R.string.handoff_message) + " " + handoff.getName(),
Snackbar.LENGTH_INDEFINITE)
.setAction(getResources().getString(R.string.end_handoff),
snackbarClickListener)
@@ -520,6 +547,10 @@
}
}
+ private void toast(String message) {
+ Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
+ }
+
public class NavigateClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
index 2f5d437..8e57ac4 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
@@ -62,7 +62,8 @@
private DrawerLayout mDrawerLayout;
private ListView mDrawerListView;
private View mFragmentContainerView;
- private JSONObject mUserProfile;
+ private String mUserEmail;
+ private String mUserName;
private int mCurrentSelectedPosition = 0;
private boolean mFromSavedInstanceState;
@@ -79,12 +80,8 @@
// drawer. See PREF_USER_LEARNED_DRAWER for details.
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mUserLearnedDrawer = prefs.getBoolean(PREF_USER_LEARNED_DRAWER, false);
- String userProfileJsonStr = prefs.getString(SignInActivity.PREF_USER_PROFILE_JSON, "");
- try {
- mUserProfile = new JSONObject(userProfileJsonStr);
- } catch (JSONException e) {
- Log.e(TAG, "Couldn't parse user profile data: " + userProfileJsonStr);
- }
+ mUserEmail = SignInActivity.getUserEmail(getActivity());
+ mUserName = SignInActivity.getUserName(getActivity());
if (savedInstanceState != null) {
mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
@@ -113,30 +110,16 @@
selectItem(position);
}
});
- mDrawerListView.setAdapter(new ArrayAdapter<JSONObject>(
+ mDrawerListView.setAdapter(new ArrayAdapter<String>(
getActionBar().getThemedContext(),
android.R.layout.simple_list_item_activated_2,
android.R.id.text1,
- new JSONObject[]{mUserProfile}) {
+ new String[]{ mUserName }) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
- JSONObject userProfile = getItem(position);
- String name = "";
- String email = "";
- if (userProfile != null) {
- try {
- name = userProfile.getString("name");
- email = userProfile.getString("email");
- } catch (JSONException e) {
- Log.e(TAG, "Error reading from user profile: " + e.getMessage());
- }
- }
- if (name.isEmpty() && email.isEmpty()) {
- name = "USER1";
- }
View view = super.getView(position, convertView, parent);
- ((TextView) view.findViewById(android.R.id.text1)).setText(name);
- ((TextView) view.findViewById(android.R.id.text2)).setText(email);
+ ((TextView) view.findViewById(android.R.id.text1)).setText(mUserName);
+ ((TextView) view.findViewById(android.R.id.text2)).setText(mUserEmail);
return view;
}
});
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationActivity.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationActivity.java
index 9ff1fa9..4d4f99c 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationActivity.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationActivity.java
@@ -6,13 +6,17 @@
import android.content.Intent;
+import android.database.Cursor;
import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.db.VPerson;
import io.v.android.apps.syncslides.discovery.ParticipantPeer;
import io.v.android.apps.syncslides.misc.Config;
import io.v.android.apps.syncslides.misc.V23Manager;
@@ -20,6 +24,7 @@
import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Participant;
import io.v.android.apps.syncslides.model.Role;
+import io.v.v23.security.Blessings;
public class PresentationActivity extends AppCompatActivity {
private static final String TAG = "PresentationActivity";
@@ -33,8 +38,21 @@
* of the activity.
*/
private Role mRole;
- // TODO(kash): Replace this with the presentation id.
- private String mPresentationId = "randomPresentationId1";
+ /**
+ * Makes decks, handles deck conversions.
+ */
+ private DeckFactory mDeckFactory;
+
+ /**
+ * The presentation ID.
+ */
+ private String mPresentationId;
+
+ /**
+ * The syncgroup name.
+ */
+ private String mSyncgroupName;
+
private boolean mSynced;
/**
@@ -52,11 +70,10 @@
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
// Initialize the DeckFactory.
- DeckFactory.Singleton.get(getApplicationContext());
+ mDeckFactory = DeckFactory.Singleton.get(getApplicationContext());
// Immediately initialize V23, possibly sending user to the
// AccountManager to get blessings.
V23Manager.Singleton.get().init(getApplicationContext(), this);
- DB.Singleton.get(getApplicationContext()).init(this);
setContentView(R.layout.activity_presentation);
mShouldBeAdvertising = false;
@@ -67,11 +84,15 @@
deckId = getIntent().getStringExtra(Deck.B.DECK_ID);
mRole = (Role) getIntent().getSerializableExtra(
Participant.B.PARTICIPANT_ROLE);
+ mPresentationId = getIntent().getStringExtra(Participant.B.PRESENTATION_ID);
+ mSyncgroupName = getIntent().getStringExtra(Participant.B.SYNCGROUP_NAME);
mSynced = true;
} else {
Log.d(TAG, "savedInstanceState is NOT null");
mRole = (Role) savedInstanceState.get(Participant.B.PARTICIPANT_ROLE);
deckId = savedInstanceState.getString(Deck.B.DECK_ID);
+ mPresentationId = savedInstanceState.getString(Participant.B.PRESENTATION_ID);
+ mSyncgroupName = savedInstanceState.getString(Participant.B.SYNCGROUP_NAME);
mSynced = savedInstanceState.getBoolean(Participant.B.PARTICIPANT_SYNCED);
mShouldBeAdvertising = savedInstanceState.getBoolean(Participant.B.PARTICIPANT_SHOULD_ADV);
if (mShouldBeAdvertising) {
@@ -79,12 +100,34 @@
}
}
- Log.d(TAG, "Role = " + mRole);
- mDeck = DB.Singleton.get(getApplicationContext()).getDeck(deckId);
- if (mDeck == null) {
- throw new IllegalArgumentException("Unusable deckId: "+ deckId);
+ // TODO(kash): This is a total hack. I thought that the deck would be
+ // loaded by this point, but we aren't actually guaranteed that. After
+ // this is fixed, we can uncomment handleError in SyncbaseDB.getDeck().
+ while ((mDeck = DB.Singleton.get(getApplicationContext()).getDeck(deckId)) == null) {
+ Log.d(TAG, "Waiting for deck to load...");
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
}
- Log.d(TAG, "Using deck: " + mDeck);
+ if (mDeck == null) {
+ throw new IllegalArgumentException("Unusable deckId: " + deckId);
+ }
+ Log.d(TAG, "Unpacked state:");
+ Log.d(TAG, " mShouldBeAdvertising = " + mShouldBeAdvertising);
+ Log.d(TAG, " mRole = " + mRole);
+ Log.d(TAG, " mPresentationId = " + mPresentationId);
+ Log.d(TAG, " mSyncgroupName = " + mSyncgroupName);
+ Log.d(TAG, " Deck = " + mDeck);
+ Log.d(TAG, " mSynced = " + mSynced);
+
+ if (mRole.equals(Role.AUDIENCE)) {
+ if (mPresentationId.equals(Participant.Unknown.PRESENTATION_ID) ||
+ mSyncgroupName.equals(Participant.Unknown.SYNCGROUP_NAME)) {
+ throw new IllegalArgumentException("Cannot be an audience.");
+ }
+ }
// TODO(jregan): This appears to be an attempt to avoid fragment
// re-inflation, possibly the right thing to do is move the code
@@ -124,6 +167,8 @@
super.onSaveInstanceState(b);
Log.d(TAG, "onSaveInstanceState");
b.putSerializable(Participant.B.PARTICIPANT_ROLE, mRole);
+ b.putString(Participant.B.PRESENTATION_ID, mPresentationId);
+ b.putString(Participant.B.SYNCGROUP_NAME, mSyncgroupName);
b.putString(Deck.B.DECK_ID, mDeck.getId());
b.putBoolean(Participant.B.PARTICIPANT_SYNCED, mSynced);
b.putBoolean(Participant.B.PARTICIPANT_SHOULD_ADV, mShouldBeAdvertising);
@@ -180,10 +225,20 @@
return;
}
if (shouldUseV23()) {
- V23Manager.Singleton.get().mount(
+ V23Manager v23Manager = V23Manager.Singleton.get();
+ Blessings blessings = v23Manager.getBlessings();
+ v23Manager.mount(
Config.MtDiscovery.makeMountName(mDeck),
- new ParticipantPeer.Server(mDeck));
- Log.d(TAG, "MT advertising started.");
+ new ParticipantPeer.Server(
+ mDeckFactory.make(mDeck),
+ mDeck.getId(),
+ new VPerson(blessings.toString(), SignInActivity.getUserName(this)),
+ mSyncgroupName,
+ mPresentationId));
+ Log.d(TAG, "MT advertising started:");
+ Log.d(TAG, " mSyncgroupName = " + mSyncgroupName);
+ Log.d(TAG, " mPresentationId = " + mPresentationId);
+ Log.d(TAG, " mDeck = " + mDeck);
} else {
Log.d(TAG, "No means to start advertising.");
}
@@ -215,15 +270,17 @@
DB db = DB.Singleton.get(getApplicationContext());
db.createPresentation(mDeck.getId(), new DB.Callback<DB.CreatePresentationResult>() {
@Override
- public void done(DB.CreatePresentationResult startPresentationResult) {
+ public void done(DB.CreatePresentationResult result) {
Log.i(TAG, "Started presentation");
Toast.makeText(getApplicationContext(), "Started presentation",
Toast.LENGTH_SHORT).show();
+ mPresentationId = result.presentationId;
+ mSyncgroupName = result.syncgroupName;
startAdvertising();
+ navigateToSlide(0);
}
});
mRole = Role.PRESENTER;
- showNavigateFragmentWithBackStack(0);
}
/**
@@ -252,21 +309,26 @@
}
/**
- * Shows the navigate fragment where the user can see the current slide and
- * navigate to other components of the slide presentation. This version
- * includes an add to the back stack so that the user can back out from the
- * navigate fragment to slide list.
+ * Shows the navigate fragment where the user can see the given slide and
+ * navigate to other components of the slide presentation. If the role is
+ * not AUDIENCE, this version includes an add to the back stack so that
+ * the user can back out from the navigate fragment to slide list.
*
- * @param slideNum the number of the current slide to show in the fragment
+ * @param slideNum the number of the slide to show in the fragment
*/
- public void showNavigateFragmentWithBackStack(int slideNum) {
+ public void navigateToSlide(int slideNum) {
+ // The user picked this specific slide. Don't try to stay synced.
+ setUnsynced();
+
NavigateFragment fragment = NavigateFragment.newInstance(
mDeck.getId(), mPresentationId, slideNum, mRole);
- getSupportFragmentManager()
+ FragmentTransaction transaction = getSupportFragmentManager()
.beginTransaction()
- .replace(R.id.fragment, fragment)
- .addToBackStack("")
- .commit();
+ .replace(R.id.fragment, fragment);
+ if (mRole != Role.AUDIENCE) {
+ transaction.addToBackStack("");
+ }
+ transaction.commit();
}
/**
@@ -300,5 +362,4 @@
public void setUnsynced() {
mSynced = false;
}
-
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/QuestionDialogFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/QuestionDialogFragment.java
index 539e081..05239e1 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/QuestionDialogFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/QuestionDialogFragment.java
@@ -12,19 +12,21 @@
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
+import java.util.List;
+
+import io.v.android.apps.syncslides.model.Question;
+
/**
* Dialog for the presenter to pick a questioner.
*/
public class QuestionDialogFragment extends DialogFragment {
- public static final String QUESTION_BUNDLE_KEY = "questioner_position";
+ public static final String QUESTION_ID_KEY = "question_id_key";
private static final String QUESTIONER_LIST_KEY = "questioner_list_key";
- private String[] mQuestionerList;
-
- public static QuestionDialogFragment newInstance(String[] questionerList) {
+ public static QuestionDialogFragment newInstance(List<Question> questions) {
QuestionDialogFragment fragment = new QuestionDialogFragment();
Bundle args = new Bundle();
- args.putStringArray(QUESTIONER_LIST_KEY, questionerList);
+ args.putParcelableArray(QUESTIONER_LIST_KEY, questions.toArray(new Question[0]));
fragment.setArguments(args);
return fragment;
}
@@ -32,12 +34,16 @@
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle args = getArguments();
- mQuestionerList = args.getStringArray(QUESTIONER_LIST_KEY);
+ final Question[] questions = (Question[]) args.getParcelableArray(QUESTIONER_LIST_KEY);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ String[] questioners = new String[questions.length];
+ for (int i = 0; i < questions.length; i++) {
+ questioners[i] = questions[i].getName();
+ }
builder.setTitle(R.string.question_message)
- .setItems(mQuestionerList, new DialogInterface.OnClickListener() {
+ .setItems(questioners, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
- sendResult(which);
+ sendResult(questions[which].getId());
}
});
return builder.create();
@@ -49,10 +55,10 @@
((PresentationActivity) getActivity()).setUiImmersive(true);
}
- // Send back the position of the questioner to the NavigateFragment.
- private void sendResult(int position) {
+ // Send back the question's ID to the NavigateFragment.
+ private void sendResult(String id) {
Intent intent = new Intent();
- intent.putExtra(QUESTION_BUNDLE_KEY, position);
+ intent.putExtra(QUESTION_ID_KEY, id);
getTargetFragment().onActivityResult(
getTargetRequestCode(), Activity.RESULT_OK, intent);
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
index 3d225bc..c760e16 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
@@ -14,10 +14,12 @@
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
@@ -42,8 +44,10 @@
public class SignInActivity extends AppCompatActivity {
private static final String TAG = "SignInActivity";
- public static final String PREF_USER_ACCOUNT_NAME = "user_account";
- public static final String PREF_USER_PROFILE_JSON = "user_profile";
+ private static final String PREF_USER_ACCOUNT_NAME = "user_account";
+ private static final String PREF_USER_NAME_FROM_CONTACTS = "user_name_from_contacts";
+ private static final String PREF_USER_NAME_FROM_PROFILE = "user_name_from_profile";
+ private static final String PREF_USER_PROFILE_JSON = "user_profile";
private static final int REQUEST_CODE_PICK_ACCOUNT = 1000;
private static final int REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL = 1001;
@@ -57,6 +61,43 @@
private String mAccountName;
private ProgressDialog mProgressDialog;
+ /**
+ * Returns the best-effort email of the signed-in user.
+ */
+ public static String getUserEmail(Context ctx) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ return prefs.getString(PREF_USER_ACCOUNT_NAME, "");
+ }
+
+ /**
+ * Returns the best-effort full name of the signed-in user.
+ */
+ public static String getUserName(Context ctx) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ // First try to read the user name we obtained from profile, as it's most accurate.
+ if (prefs.contains(PREF_USER_NAME_FROM_PROFILE)) {
+ return prefs.getString(PREF_USER_NAME_FROM_PROFILE, "Anonymous User");
+ }
+ return prefs.getString(PREF_USER_NAME_FROM_CONTACTS, "Anonymous User");
+ }
+
+ /**
+ * Returns the Google profile information of the signed-in user, or {@code null} if the
+ * profile information couldn't be retrieved.
+ */
+ public static JSONObject getUserProfile(Context ctx) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ String userProfileJsonStr = prefs.getString(PREF_USER_PROFILE_JSON, "");
+ if (!userProfileJsonStr.isEmpty()) {
+ try {
+ return new JSONObject(userProfileJsonStr);
+ } catch (JSONException e) {
+ Log.e(TAG, "Couldn't parse user profile data: " + userProfileJsonStr);
+ }
+ }
+ return null;
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -107,7 +148,33 @@
editor.putString(PREF_USER_ACCOUNT_NAME, accountName);
editor.commit();
- fetchUserProfile();
+ fetchUserNameFromContacts();
+
+ // NOTE(spetrovic): For demo purposes, fetching user profile is too risky as it requires
+ // internet access. So we disable it for now.
+ //fetchUserProfile();
+ finishActivity();
+ }
+
+ private void fetchUserNameFromContacts() {
+ // Get the user's full name from Contacts.
+ Cursor c = getContentResolver().query(ContactsContract.Profile.CONTENT_URI,
+ null, null, null, null);
+ String[] columnNames = c.getColumnNames();
+ String userName = "Anonymous User";
+ while (c.moveToNext()) {
+ for (int j = 0; j < columnNames.length; j++) {
+ String columnName = columnNames[j];
+ if (!columnName.equals(ContactsContract.Contacts.DISPLAY_NAME)) {
+ continue;
+ }
+ userName = c.getString(c.getColumnIndex(columnName));
+ }
+ }
+ c.close();
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putString(PREF_USER_NAME_FROM_CONTACTS, userName);
+ editor.commit();
}
private void fetchUserProfile() {
@@ -144,9 +211,15 @@
if (userProfile != null) {
SharedPreferences.Editor editor = mPrefs.edit();
editor.putString(PREF_USER_PROFILE_JSON, userProfile.toString());
+ try {
+ if (userProfile.has("name") && !userProfile.getString("name").isEmpty()) {
+ editor.putString(PREF_USER_NAME_FROM_PROFILE, userProfile.getString("name"));
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Couldn't read user name from user profile: " + e.getMessage());
+ }
editor.commit();
}
-
finishActivity();
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListAdapter.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListAdapter.java
index aeb1ed6..9afae0d 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListAdapter.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListAdapter.java
@@ -36,7 +36,7 @@
@Override
public void onClick(View v) {
int position = mRecyclerView.getChildAdapterPosition(v);
- ((PresentationActivity) v.getContext()).showNavigateFragmentWithBackStack(position);
+ ((PresentationActivity) v.getContext()).navigateToSlide(position);
}
});
return new ViewHolder(v);
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/CurrentSlideWatcher.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/CurrentSlideWatcher.java
index a221d0a..beab3d1 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/CurrentSlideWatcher.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/CurrentSlideWatcher.java
@@ -101,7 +101,7 @@
Log.i(TAG, "notifyListeners " + slide);
mCurrentSlide = slide;
for (DB.CurrentSlideListener listener : mListeners) {
- Log.i(TAG, "notifying listner " + listener);
+ Log.i(TAG, "notifying listener " + listener);
listener.onChange(mCurrentSlide.getNum());
}
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java
index 668b7e0..e84b42d 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java
@@ -4,7 +4,6 @@
package io.v.android.apps.syncslides.db;
-import android.app.Activity;
import android.content.Context;
import java.util.List;
@@ -39,14 +38,9 @@
}
/**
- * Perform initialization steps. This method must be called early in the
- * lifetime of the activity. As part of the initialization, it might send
- * an intent to another activity.
- *
- * @param activity implements onActivityResult() to call into
- * DB.onActivityResult.
+ * Perform initialization steps.
*/
- void init(Activity activity);
+ void init();
/**
* Provides a list of elements via an API that fits well with
@@ -190,6 +184,15 @@
*/
void setCurrentSlide(String deckId, String presentationId, int slideNum);
+ /**
+ * Saves notes for the specified slide.
+ *
+ * @param deckId the deck being presented
+ * @param slideNum the current slide number
+ * @param slideNotes the text of the slide's notes
+ */
+ void setSlideNotes(String deckId, int slideNum, String slideNotes);
+
interface CurrentSlideListener {
/**
* Called whenever the current slide of a live presentation changes.
@@ -254,9 +257,52 @@
*
* @param deckId the deck used in the presentation
* @param presentationId the presentation identifier
- * @param firstName the user's first name
- * @param lastName the user's last name
+ * @param name the user's full name
*/
- void askQuestion(String deckId, String presentationId,
- String firstName, String lastName);
+ void askQuestion(String deckId, String presentationId, String name);
+
+ /**
+ * Give control of the presentation to the questioner. Mark the question as answered.
+ *
+ * @param deckId the deck used in the presentation
+ * @param presentationId the presentation identifier
+ * @param questionId the question being answered
+ */
+ void handoffQuestion(String deckId, String presentationId, String questionId);
+
+ /**
+ * Take back control of the presentation from the questioner.
+ *
+ * @param deckId the deck used in the presentation
+ * @param presentationId the presentation identifier
+ */
+ void resumeControl(String deckId, String presentationId);
+
+ interface DriverListener {
+ /**
+ * Called whenever the presentation's driver changes.
+ *
+ * @param driver the temporary driver of the presentation. null means that the
+ * original presenter has control.
+ */
+ void onChange(VPerson driver);
+ }
+
+ /**
+ * Set the listener for changes to the driver of the presentation.
+ *
+ * @param deckId the deck used in the presentation
+ * @param presentationId the presentation identifier
+ * @param listener notified of changes
+ */
+ void setDriverListener(String deckId, String presentationId, DriverListener listener);
+
+ /**
+ * Remove the listener that was previously passed to setDriverListener().
+ *
+ * @param deckId the deck used in the presentation
+ * @param presentationId the presentation identifier
+ * @param listener previously passed to setDriverListener()
+ */
+ void removeDriverListener(String deckId, String presentationId, DriverListener listener);
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DriverWatcher.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DriverWatcher.java
new file mode 100644
index 0000000..0e29070
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DriverWatcher.java
@@ -0,0 +1,103 @@
+// 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.syncslides.db;
+
+import android.util.Log;
+
+import io.v.impl.google.naming.NamingUtil;
+import io.v.v23.syncbase.nosql.BatchDatabase;
+import io.v.v23.syncbase.nosql.ChangeType;
+import io.v.v23.syncbase.nosql.Stream;
+import io.v.v23.syncbase.nosql.Table;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+
+/**
+ * Watches for a new driver (person controlling the slides) in a given presentation.
+ */
+class DriverWatcher {
+ private static final String TAG = "DriverWatcher";
+ private final WatcherState mState;
+ private final DB.DriverListener mListener;
+ private boolean mIsDiscarded;
+
+ /**
+ * Creates a new watcher for the presentation in state.
+ *
+ * @param state objects necessary to do the watching
+ * @param listener notified whenever the driver changes
+ */
+ public DriverWatcher(WatcherState state, DB.DriverListener listener) {
+ mState = state;
+ mListener = listener;
+ mState.thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ watch();
+ }
+ });
+ mState.thread.start();
+ mIsDiscarded = false;
+ }
+
+ /**
+ * Stops watching the presentation for a new driver.
+ */
+ public void discard() {
+ mState.vContext.cancel(); // this will cause the watcher thread to exit
+ mState.handler.removeCallbacksAndMessages(null);
+ // We've canceled all the pending callbacks, but the handler might be just about
+ // to execute put()/get() and those messages wouldn't get canceled. So we mark
+ // the list as discarded and count on put()/get() checking for it.
+ mIsDiscarded = true;
+ }
+
+ private void watch() {
+ try {
+ String row = NamingUtil.join(mState.deckId, mState.presentationId);
+ Log.i(TAG, "Watching driver: " + row);
+ BatchDatabase batch = mState.db.beginBatch(mState.vContext, null);
+ Table presentations = batch.getTable(SyncbaseDB.PRESENTATIONS_TABLE);
+ if (presentations.getRow(row).exists(mState.vContext)) {
+ VPresentation presentation = (VPresentation) presentations.get(
+ mState.vContext, row, VPresentation.class);
+ postInUiThread(presentation.getDriver().getElem());
+ }
+
+ Stream<WatchChange> watch = mState.db.watch(
+ mState.vContext, SyncbaseDB.PRESENTATIONS_TABLE, row,
+ batch.getResumeMarker(mState.vContext));
+ for (WatchChange change : watch) {
+ Log.i(TAG, "Found change " + change.getChangeType());
+ if (!change.getRowName().equals(row)) {
+ continue;
+ }
+ if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
+ final VPresentation presentation = (VPresentation) VomUtil.decode(
+ change.getVomValue(), VPresentation.class);
+ postInUiThread(presentation.getDriver().getElem());
+ } else { // ChangeType.DELETE_CHANGE
+ postInUiThread(null);
+ }
+ }
+ } catch (VException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void postInUiThread(final VPerson driver) {
+ mState.handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!mIsDiscarded) {
+ mListener.onChange(driver);
+ }
+ }
+ });
+
+ }
+}
+
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/FakeDB.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/FakeDB.java
index cd41a7d..acf1be2 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/FakeDB.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/FakeDB.java
@@ -4,7 +4,6 @@
package io.v.android.apps.syncslides.db;
-import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -105,9 +104,9 @@
for (int i = 0; i < 5; i++) {
Question question = new Question(
"question" + i,
- "Questioner",
- "#" + i,
- DateTime.now().minus(Period.minutes(random.nextInt(5))));
+ "Questioner #" + i,
+ DateTime.now().minus(Period.minutes(random.nextInt(5)))
+ .toInstant().getMillis());
mQuestions.add(question);
}
mQuestionListeners = Lists.newArrayList();
@@ -121,7 +120,7 @@
}
private static class FakeSlide implements Slide {
- private final String mSlideNotes;
+ private String mSlideNotes;
private final Bitmap mSlideImage;
FakeSlide(Bitmap slideImage, String slideNotes) {
@@ -138,6 +137,11 @@
public String getNotes() {
return mSlideNotes;
}
+
+ @Override
+ public void setNotes(String notes) {
+ mSlideNotes = notes;
+ }
}
private static class FakeDeckList implements DBList<Deck> {
@@ -217,17 +221,36 @@
@Override
- public void init(Activity activity) {
+ public void init() {
// Nothing to do.
}
@Override
- public void askQuestion(String deckId, String presentationId,
- String firstName, String lastName) {
+ public void askQuestion(String deckId, String presentationId, String name) {
// Nothing to do.
}
@Override
+ public void handoffQuestion(String deckId, String presentationId, String questionId) {
+ // Not implemented.
+ }
+
+ @Override
+ public void resumeControl(String deckId, String presentationId) {
+ // Not implemented.
+ }
+
+ @Override
+ public void setDriverListener(String deckId, String presentationId, DriverListener listener) {
+ // Not implemented.
+ }
+
+ @Override
+ public void removeDriverListener(String deckId, String presentationId, DriverListener listener) {
+ // Not implemented.
+ }
+
+ @Override
public DBList<Deck> getDecks() {
return mDecks;
}
@@ -262,6 +285,11 @@
}
@Override
+ public void setSlideNotes(String deckId, int slideNum, String slideNotes) {
+
+ }
+
+ @Override
public void addCurrentSlideListener(String deckId, String presentationId,
CurrentSlideListener listener) {
mCurrentSlideListeners.add(listener);
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/QuestionWatcher.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/QuestionWatcher.java
index 7689969..2c82486 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/QuestionWatcher.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/QuestionWatcher.java
@@ -76,11 +76,13 @@
Stream<KeyValue> stream = presentations.scan(mState.vContext, RowRange.prefix(prefix));
for (KeyValue keyValue : stream) {
VQuestion value = (VQuestion) VomUtil.decode(keyValue.getValue(), VQuestion.class);
+ if (value.getAnswered()) {
+ continue;
+ }
final Question question = new Question(
- keyValue.getKey(),
- value.getQuestioner().getFirstName(),
- value.getQuestioner().getLastName(),
- new DateTime(value.getTime()));
+ lastPart(keyValue.getKey()),
+ value.getQuestioner().getName(),
+ value.getTime());
mState.handler.post(new Runnable() {
@Override
public void run() {
@@ -94,15 +96,25 @@
batch.getResumeMarker(mState.vContext));
for (WatchChange change : watch) {
Log.i(TAG, "Found change " + change.getChangeType());
+ final String id = lastPart(change.getRowName());
if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
VQuestion vQuestion = (VQuestion) VomUtil.decode(
change.getVomValue(), VQuestion.class);
Log.i(TAG, "Change " + change);
+ if (vQuestion.getAnswered()) {
+ mState.handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Log.i(TAG, "Question was answered");
+ delete(id);
+ }
+ });
+ continue;
+ }
final Question question = new Question(
- change.getRowName(),
- vQuestion.getQuestioner().getFirstName(),
- vQuestion.getQuestioner().getLastName(),
- new DateTime(vQuestion.getTime()));
+ id,
+ vQuestion.getQuestioner().getName(),
+ vQuestion.getTime());
mState.handler.post(new Runnable() {
@Override
public void run() {
@@ -110,7 +122,6 @@
}
});
} else { // ChangeType.DELETE_CHANGE
- final String id = change.getRowName();
mState.handler.post(new Runnable() {
@Override
public void run() {
@@ -156,8 +167,17 @@
if (mQuestions.get(i).getId().equals(id)) {
mQuestions.remove(i);
mListener.onChange(Lists.newArrayList(mQuestions));
+ return;
}
}
+ Log.i(TAG, "Could not find question " + id);
}
+ /**
+ * Splits the name into parts and returns the last one.
+ */
+ private static String lastPart(String name) {
+ List<String> split = NamingUtil.split(name);
+ return split.get(split.size() - 1);
+ }
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
index a599bdb..a8a8039 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
@@ -4,7 +4,6 @@
package io.v.android.apps.syncslides.db;
-import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -45,11 +44,13 @@
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.access.AccessList;
import io.v.v23.security.access.Constants;
import io.v.v23.security.access.Permissions;
+import io.v.v23.services.syncbase.nosql.PrefixPermissions;
import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
-import io.v.v23.services.syncbase.nosql.SyncgroupPrefix;
+import io.v.v23.services.syncbase.nosql.TableRow;
import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
import io.v.v23.services.watch.ResumeMarker;
import io.v.v23.syncbase.Syncbase;
@@ -65,6 +66,7 @@
import io.v.v23.syncbase.nosql.Table;
import io.v.v23.syncbase.nosql.WatchChange;
import io.v.v23.vdl.VdlAny;
+import io.v.v23.vdl.VdlOptional;
import io.v.v23.verror.Errors;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
@@ -92,6 +94,7 @@
private Table mPresentations;
private final Map<String, CurrentSlideWatcher> mCurrentSlideWatchers;
private final Map<String, QuestionWatcher> mQuestionWatchers;
+ private final Map<String, DriverWatcher> mDriverWatchers;
private Server mSyncbaseServer;
private final DeckFactory mDeckFactory;
@@ -99,25 +102,18 @@
mContext = context;
mCurrentSlideWatchers = Maps.newHashMap();
mQuestionWatchers = Maps.newHashMap();
+ mDriverWatchers = Maps.newHashMap();
mDeckFactory = DeckFactory.Singleton.get(context);
}
@Override
- public void init(Activity activity) {
+ public void init() {
Log.d(TAG, "init");
if (mInitialized) {
Log.d(TAG, "already initialized");
return;
}
mHandler = new Handler(Looper.getMainLooper());
- // TODO(kash): Set proper ACLs.
- AccessList acl = new AccessList(
- ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
- mPermissions = new Permissions(ImmutableMap.of(
- Constants.RESOLVE.getValue(), acl,
- Constants.READ.getValue(), acl,
- Constants.WRITE.getValue(), acl,
- Constants.ADMIN.getValue(), acl));
// If blessings aren't in place, the fragment that called this
// initialization may continue to load and use DB, but nothing will
@@ -134,6 +130,19 @@
// TODO(kash): Run this in an AsyncTask so it doesn't block the UI.
private void setupSyncbase() {
+ Blessings blessings = V23Manager.Singleton.get().getBlessings();
+ AccessList everyoneAcl = new AccessList(
+ ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
+ AccessList justMeAcl = new AccessList(
+ ImmutableList.of(new BlessingPattern(blessings.toString())),
+ ImmutableList.<String>of());
+
+ mPermissions = new Permissions(ImmutableMap.of(
+ Constants.RESOLVE.getValue(), everyoneAcl,
+ Constants.READ.getValue(), justMeAcl,
+ Constants.WRITE.getValue(), justMeAcl,
+ Constants.ADMIN.getValue(), justMeAcl));
+
// Prepare the syncbase storage directory.
File storageDir = new File(mContext.getFilesDir(), "syncbase");
storageDir.mkdirs();
@@ -180,7 +189,7 @@
if (!mPresentations.exists(mVContext)) {
mPresentations.create(mVContext, mPermissions);
}
- importDecks();
+ //importDecks();
} catch (VException e) {
handleError("Couldn't setup syncbase service: " + e.getMessage());
return;
@@ -201,10 +210,27 @@
private void createPresentationRunnable(final String deckId,
final Callback<CreatePresentationResult> callback) {
- //final String presentationId = UUID.randomUUID().toString();
- final String presentationId = "randomPresentationId1";
+ final String presentationId = UUID.randomUUID().toString();
String prefix = NamingUtil.join(deckId, presentationId);
try {
+ Blessings blessings = V23Manager.Singleton.get().getBlessings();
+ AccessList everyoneAcl = new AccessList(
+ ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
+ AccessList justMeAcl = new AccessList(
+ ImmutableList.of(new BlessingPattern(blessings.toString())),
+ ImmutableList.<String>of());
+
+ Permissions groupReadPermissions = new Permissions(ImmutableMap.of(
+ Constants.RESOLVE.getValue(), everyoneAcl,
+ Constants.READ.getValue(), everyoneAcl,
+ Constants.WRITE.getValue(), justMeAcl,
+ Constants.ADMIN.getValue(), justMeAcl));
+ Permissions groupWritePermissions = new Permissions(ImmutableMap.of(
+ Constants.RESOLVE.getValue(), everyoneAcl,
+ Constants.READ.getValue(), everyoneAcl,
+ Constants.WRITE.getValue(), justMeAcl,
+ Constants.ADMIN.getValue(), justMeAcl));
+
// Add rows to Presentations table.
VPresentation presentation = new VPresentation(); // Empty for now.
mPresentations.put(mVContext, prefix, presentation, VPresentation.class);
@@ -213,15 +239,19 @@
current, VCurrentSlide.class);
mPresentations.setPrefixPermissions(mVContext, RowRange.prefix(prefix),
- mPermissions);
- mDecks.setPrefixPermissions(mVContext, RowRange.prefix(deckId), mPermissions);
+ groupReadPermissions);
+ // Anybody can add a question.
+ mPresentations.setPrefixPermissions(
+ mVContext,
+ RowRange.prefix(NamingUtil.join(prefix, QUESTIONS)),
+ groupWritePermissions);
+ mDecks.setPrefixPermissions(mVContext, RowRange.prefix(deckId), groupReadPermissions);
// Create the syncgroup.
final String syncgroupName = NamingUtil.join(
mSyncbaseServer.getStatus().getMounts()[0].getName(),
"%%sync/syncslides",
prefix);
- //final String syncgroupName = STATIC_SYNCGROUP;
Log.i(TAG, "Creating syncgroup " + syncgroupName);
Syncgroup syncgroup = mDB.getSyncgroup(syncgroupName);
CancelableVContext context = mVContext.withTimeout(Duration.millis(5000));
@@ -230,11 +260,10 @@
context,
new SyncgroupSpec(
SYNCGROUP_PRESENTATION_DESCRIPTION,
- // TODO(kash): Use real permissions.
- mPermissions,
+ groupReadPermissions,
Arrays.asList(
- new SyncgroupPrefix(PRESENTATIONS_TABLE, prefix),
- new SyncgroupPrefix(DECKS_TABLE, deckId)),
+ new TableRow(PRESENTATIONS_TABLE, prefix),
+ new TableRow(DECKS_TABLE, deckId)),
Arrays.asList(V23Manager.syncName("sg")),
false
),
@@ -248,8 +277,6 @@
}
Log.i(TAG, "Finished creating syncgroup");
- V23Manager.Singleton.get().scan("...");
-
// TODO(kash): Create a syncgroup for Notes? Not sure if we should do that
// here or somewhere else. We're not going to demo sync across a user's
// devices right away, so we'll figure this out later.
@@ -283,11 +310,11 @@
try {
Log.i(TAG, "Joining: " + syncgroupName);
Syncgroup syncgroup = mDB.getSyncgroup(syncgroupName);
+ Log.d(TAG, "syncgroup = " + syncgroup);
syncgroup.join(mVContext, new SyncgroupMemberInfo((byte) 1));
for (String member : syncgroup.getMembers(mVContext).keySet()) {
Log.i(TAG, "Member: " + member);
}
- V23Manager.Singleton.get().scan("...");
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
@@ -522,11 +549,8 @@
String key = (String) row.get(0).getElem();
Log.i(TAG, "Fetched slide " + key);
VSlide slide = (VSlide) row.get(1).getElem();
- VNote note = (VNote) table.get(mVContext, key, VNote.class);
- slides.add(new SlideImpl(
- BitmapFactory.decodeByteArray(
- slide.getThumbnail(), 0, slide.getThumbnail().length),
- note.getText()));
+ String note = notesForSlide(mVContext, table, key);
+ slides.add(new SlideImpl(slide.getThumbnail(), note));
}
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@@ -600,11 +624,8 @@
final String key = (String) row.get(0).getElem();
Log.i(TAG, "Fetched slide " + key);
VSlide slide = (VSlide) row.get(1).getElem();
- String notes = notesForSlide(notesTable, key);
- final SlideImpl newSlide = new SlideImpl(
- BitmapFactory.decodeByteArray(
- slide.getThumbnail(), 0, slide.getThumbnail().length),
- notes);
+ String notes = notesForSlide(mVContext, notesTable, key);
+ final SlideImpl newSlide = new SlideImpl(slide.getThumbnail(), notes);
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -648,11 +669,8 @@
} catch (VException e) {
Log.e(TAG, "Couldn't decode slide: " + e.toString());
}
- String notes = notesForSlide(notesTable, key);
- final SlideImpl slide = new SlideImpl(
- BitmapFactory.decodeByteArray(
- vSlide.getThumbnail(), 0, vSlide.getThumbnail().length),
- notes);
+ String notes = notesForSlide(mVContext, notesTable, key);
+ final SlideImpl slide = new SlideImpl(vSlide.getThumbnail(), notes);
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -747,12 +765,15 @@
}
}
- private String notesForSlide(Table notesTable, String key) {
- try {
- return ((VNote) notesTable.get(mVContext, key, VNote.class)).getText();
- } catch (VException e) {
- return "";
- }
+ }
+
+ private static String notesForSlide(VContext context, Table notesTable, String key) {
+ try {
+ return ((VNote) notesTable.get(context, key, VNote.class)).getText();
+ } catch (VException e) {
+ // TODO(kash): Should really differentiate between row not existing
+ // and some server error.
+ return "";
}
}
@@ -775,6 +796,27 @@
}
@Override
+ public void setSlideNotes(final String deckId, final int slideNum, final String slideNotes) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ String rowKey = slideRowKey(deckId, slideNum);
+ Log.i(TAG, "Saving notes " + rowKey + " with " + slideNotes);
+ Table notesTable = mDB.getTable(NOTES_TABLE);
+ notesTable.put(mVContext, rowKey, new VNote(slideNotes), VNote.class);
+ } catch (VException e) {
+ handleError(e.toString());
+ }
+ }
+ }).start();
+ }
+
+ private String slideRowKey(String deckId, int slideNum) {
+ return NamingUtil.join(deckId, "slides", String.format("%04d", slideNum));
+ }
+
+ @Override
public void addCurrentSlideListener(String deckId, String presentationId,
CurrentSlideListener listener) {
String key = NamingUtil.join(deckId, presentationId);
@@ -827,21 +869,47 @@
@Override
public void askQuestion(final String deckId, final String presentationId,
- final String firstName, final String lastName) {
+ final String personName) {
+ final Blessings blessings = V23Manager.Singleton.get().getBlessings();
new Thread(new Runnable() {
@Override
public void run() {
try {
String rowKey = NamingUtil.join(deckId, presentationId, QUESTIONS,
UUID.randomUUID().toString());
- Log.i(TAG, "Writing row " + rowKey + " with " + firstName + " " + lastName);
+ Log.i(TAG, "Writing row " + rowKey + " with " + personName);
+ BatchDatabase batch = mDB.beginBatch(mVContext, null);
+ Table presentations = batch.getTable(PRESENTATIONS_TABLE);
VQuestion question = new VQuestion(
- new VPerson(firstName, lastName),
+ new VPerson(blessings.toString(), personName),
System.currentTimeMillis(),
false // Not yet answered.
);
- mPresentations.put(mVContext, rowKey, question, VQuestion.class);
- // TODO(kash): Set the ACL so nobody else can modify the question.
+ presentations.put(mVContext, rowKey, question, VQuestion.class);
+
+ // There should be PrefixPermissions on
+ // <deckId>/<presentationId>/questions
+ // <deckId>/<presentationId>
+ // ""
+ // We want to copy <deckId>/<presentationId> for our question but also
+ // add ourselves.
+ PrefixPermissions[] permissions =
+ presentations.getPrefixPermissions(mVContext, rowKey);
+ for (PrefixPermissions prefix : permissions) {
+ Log.i(TAG, "Found permissions: " + prefix.toString());
+ }
+ if (permissions.length < 3) {
+ handleError("Missing permissions for questions: "
+ + Arrays.toString(permissions));
+ batch.abort(mVContext);
+ return;
+ }
+ Permissions perms = permissions[1].getPerms();
+ perms.get(Constants.WRITE.getValue()).getIn().add(
+ new BlessingPattern(blessings.toString()));
+ presentations.setPrefixPermissions(mVContext, RowRange.prefix(rowKey), perms);
+
+ batch.commit(mVContext);
} catch (VException e) {
handleError(e.toString());
}
@@ -849,13 +917,142 @@
}).start();
}
- private void handleError(String msg) {
+ @Override
+ public void handoffQuestion(final String deckId, final String presentationId,
+ final String questionId) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BatchDatabase batch = mDB.beginBatch(mVContext, null);
+ Table presentations = batch.getTable(PRESENTATIONS_TABLE);
+
+ // Mark the question as answered so it disappears from the list.
+ String questionKey =
+ NamingUtil.join(deckId, presentationId, QUESTIONS, questionId);
+ VQuestion question =
+ (VQuestion) presentations.get(mVContext, questionKey, VQuestion.class);
+ if (question.getAnswered()) {
+ // Already answered. Nothing to do.
+ batch.abort(mVContext);
+ return;
+ }
+ question.setAnswered(true);
+ presentations.put(mVContext, questionKey, question, VQuestion.class);
+
+ // Set the questioner as the driver of the presentation.
+ String presentationKey = NamingUtil.join(deckId, presentationId);
+ VPresentation presentation = new VPresentation();
+ presentation.setDriver(VdlOptional.of(question.getQuestioner()));
+ presentations.put(mVContext, presentationKey, presentation,
+ VPresentation.class);
+
+ // Give the questioner permission to set the current slide.
+ String currentSlideKey = NamingUtil.join(deckId, presentationId, CURRENT_SLIDE);
+ Blessings blessings = V23Manager.Singleton.get().getBlessings();
+ AccessList everyoneAcl = new AccessList(
+ ImmutableList.of(new BlessingPattern("...")),
+ ImmutableList.<String>of());
+ AccessList questionerAcl = new AccessList(
+ ImmutableList.of(
+ new BlessingPattern(question.getQuestioner().getBlessing())),
+ ImmutableList.<String>of());
+ AccessList justMeAcl = new AccessList(
+ ImmutableList.of(new BlessingPattern(blessings.toString())),
+ ImmutableList.<String>of());
+ Permissions permissions = new Permissions(ImmutableMap.of(
+ Constants.RESOLVE.getValue(), everyoneAcl,
+ Constants.READ.getValue(), everyoneAcl,
+ Constants.WRITE.getValue(), questionerAcl,
+ Constants.ADMIN.getValue(), justMeAcl));
+ presentations.setPrefixPermissions(mVContext, RowRange.prefix(currentSlideKey),
+ permissions);
+
+ batch.commit(mVContext);
+ } catch (VException e) {
+ handleError(e.toString());
+ }
+ }
+ }).start();
+ }
+
+ @Override
+ public void resumeControl(final String deckId, final String presentationId) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BatchDatabase batch = mDB.beginBatch(mVContext, null);
+ Table presentations = batch.getTable(PRESENTATIONS_TABLE);
+
+ // Clear the driver of the presentation.
+ String presentationKey = NamingUtil.join(deckId, presentationId);
+ VPresentation presentation = new VPresentation();
+ presentations.put(mVContext, presentationKey, presentation,
+ VPresentation.class);
+
+ // Give the questioner permission to set the current slide.
+ String currentSlideKey = NamingUtil.join(deckId, presentationId, CURRENT_SLIDE);
+ Blessings blessings = V23Manager.Singleton.get().getBlessings();
+ AccessList everyoneAcl = new AccessList(
+ ImmutableList.of(new BlessingPattern("...")),
+ ImmutableList.<String>of());
+ AccessList justMeAcl = new AccessList(
+ ImmutableList.of(new BlessingPattern(blessings.toString())),
+ ImmutableList.<String>of());
+ Permissions permissions = new Permissions(ImmutableMap.of(
+ Constants.RESOLVE.getValue(), everyoneAcl,
+ Constants.READ.getValue(), everyoneAcl,
+ Constants.WRITE.getValue(), justMeAcl,
+ Constants.ADMIN.getValue(), justMeAcl));
+ presentations.setPrefixPermissions(mVContext, RowRange.prefix(currentSlideKey),
+ permissions);
+
+ batch.commit(mVContext);
+ } catch (VException e) {
+ handleError(e.toString());
+ }
+ }
+ }).start();
+ }
+
+ @Override
+ public void setDriverListener(String deckId, String presentationId, DriverListener listener) {
+ String key = NamingUtil.join(deckId, presentationId);
+ DriverWatcher oldWatcher = mDriverWatchers.get(key);
+ if (oldWatcher != null) {
+ oldWatcher.discard();
+ }
+ DriverWatcher watcher = new DriverWatcher(
+ new WatcherState(mVContext, mDB, deckId, presentationId),
+ listener);
+ mDriverWatchers.put(key, watcher);
+ }
+
+ @Override
+ public void removeDriverListener(String deckId, String presentationId,
+ DriverListener listener) {
+ String key = NamingUtil.join(deckId, presentationId);
+ DriverWatcher oldWatcher = mDriverWatchers.get(key);
+ if (oldWatcher != null) {
+ mDriverWatchers.remove(oldWatcher);
+ oldWatcher.discard();
+ }
+ }
+
+
+ private void handleError(final String msg) {
Log.e(TAG, msg);
- Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();
+ }
+ });
}
private void importDecks() {
- importDeckFromResources("deckId1", "Car Business", R.drawable.thumb_deck1);
+ importDeckFromResources(UUID.randomUUID().toString(), "Car Business", R.drawable.thumb_deck1);
// These slow down app startup, so skip these for now.
// importDeckFromResources("deckId2", "Baku Discovery", R.drawable.thumb_deck2);
// importDeckFromResources("deckId3", "Vanadium", R.drawable.thumb_deck3);
@@ -942,7 +1139,7 @@
}
private void putSlide(String prefix, int idx, byte[] thumbData, String note) throws VException {
- String key = NamingUtil.join(prefix, "slides", String.format("%04d", idx));
+ String key = slideRowKey(prefix, idx);
Log.i(TAG, "Adding slide " + key);
if (!mDecks.getRow(key).exists(mVContext)) {
mDecks.put(
@@ -966,7 +1163,9 @@
try {
vDeck = (VDeck) mDecks.get(mVContext, deckId, VDeck.class);
} catch (VException e) {
- handleError(e.toString());
+ // TODO(kash): Uncomment this when PresentationActivity.onCreate no
+ // longer needs to loop on getDeck(), waiting for it to return non-null.
+ //handleError(e.toString());
}
if (vDeck != null) {
return mDeckFactory.make(vDeck, deckId);
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/schema.vdl b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/schema.vdl
index f6c0077..6e3ac4b 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/schema.vdl
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/schema.vdl
@@ -33,8 +33,9 @@
// Person represents either an audience member or the presenter.
type VPerson struct{
- FirstName string
- LastName string
+ Blessing string
+ // The person's full name.
+ Name string
}
// CurrentSlide contains state for the live presentation. It is separate from the
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/DiscoveryManager.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/DiscoveryManager.java
index 57321c8..4aed438 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/DiscoveryManager.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/DiscoveryManager.java
@@ -109,9 +109,13 @@
return mParticipants.size();
}
+ public Participant getParticipant(int i) {
+ return mParticipants.get(i);
+ }
+
@Override
public Deck get(int i) {
- return mParticipants.get(i).getDeck();
+ return getParticipant(i).getDeck();
}
@Override
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java
index ff7e70c..b491a99 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java
@@ -8,12 +8,15 @@
import android.util.Log;
import org.joda.time.DateTime;
+import org.joda.time.Duration;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.io.ByteArrayOutputStream;
import io.v.android.apps.syncslides.db.VDeck;
+import io.v.android.apps.syncslides.db.VPerson;
+import io.v.android.apps.syncslides.discovery.Presentation;
import io.v.android.apps.syncslides.misc.V23Manager;
import io.v.android.apps.syncslides.model.Deck;
import io.v.android.apps.syncslides.model.DeckFactory;
@@ -43,19 +46,22 @@
DateTimeFormat.forPattern("hh_mm_ss_SSSS");
// V23 name of the V23 service representing the participant.
private final String mServiceName;
- // Visible name of human presenter.
- // TODO(jregan): Switch to VPerson or the model equivalent.
- private String mUserName;
+ // Person presenting.
+ private VPerson mUser;
// When did we last grab data from the endPoint?
private DateTime mRefreshTime;
// Deck the user is presenting. Can only present one at a time.
private Deck mDeck;
+ // The presentation ID.
+ private String mPresentationId;
+ // The syncgroup name.
+ private String mSyncgroupName;
// Used to make decks after RPCs.
private final DeckFactory mDeckFactory;
private ParticipantPeer(
- String userName, Deck deck, String serviceName, DeckFactory deckFactory) {
- mUserName = userName;
+ VPerson user, Deck deck, String serviceName, DeckFactory deckFactory) {
+ mUser = user;
mDeck = deck;
mServiceName = serviceName;
mDeckFactory = deckFactory;
@@ -63,11 +69,13 @@
public static ParticipantPeer makeWithServiceName(
String serviceName, DeckFactory deckFactory) {
- return new ParticipantPeer(Unknown.USER_NAME, null, serviceName, deckFactory);
+ return new ParticipantPeer(
+ // The person and deck are obtained from the given service.
+ null, null, serviceName, deckFactory);
}
- public static ParticipantPeer makeWithKnownDeck(String userName, Deck deck) {
- return new ParticipantPeer(userName, deck, Unknown.SERVER_NAME, null);
+ public static ParticipantPeer makeWithKnownDeck(VPerson user, Deck deck) {
+ return new ParticipantPeer(user, deck, Unknown.SERVER_NAME, null);
}
@Override
@@ -77,8 +85,8 @@
}
@Override
- public String getUserName() {
- return mUserName;
+ public VPerson getUser() {
+ return mUser;
}
@Override
@@ -87,8 +95,18 @@
}
@Override
+ public String getPresentationId() {
+ return mPresentationId;
+ }
+
+ @Override
+ public String getSyncgroupName() {
+ return mSyncgroupName;
+ }
+
+ @Override
public String toString() {
- return "[userName=\"" + mUserName +
+ return "[user=\"" + mUser +
"\", deck=" + mDeck +
", time=" + getStringRefreshtime() + "]";
}
@@ -132,20 +150,18 @@
ParticipantClientFactory.getParticipantClient(mServiceName);
Log.d(TAG, "Got client = " + client.toString());
try {
- Log.d(TAG, "Calling get...");
- VDeck vDeck = client.get(
- V23Manager.Singleton.get().getVContext());
- Log.d(TAG, "Back with vDeck = "+ vDeck.toString());
- byte[] bytes = vDeck.getThumbnail();
- if (bytes != null && bytes.length > 0) {
- Log.d(TAG, " Seem to have a thumb");
- } else {
- Log.d(TAG, " No thumb");
- }
- Deck newDeck = mDeckFactory.make(vDeck, "whatShouldTheIdBe");
+ Log.d(TAG, "Starting RPC to service \"" + mServiceName + "\"...");
+ Presentation p = client.get(
+ V23Manager.Singleton.get().getVContext().withTimeout(
+ Duration.standardSeconds(5)));
+ mDeck = mDeckFactory.make(p.getDeck(), p.getDeckId());
+ mSyncgroupName = p.getSyncgroupName();
+ mPresentationId = p.getPresentationId();
mRefreshTime = DateTime.now();
- mDeck = newDeck;
- Log.d(TAG, " Got deck = " + mDeck);
+ Log.d(TAG, " Discovered:");
+ Log.d(TAG, " mDeck = " + mDeck);
+ Log.d(TAG, " mSyncgroupName = " + mSyncgroupName);
+ Log.d(TAG, " mPresentationId = " + mPresentationId);
return true;
} catch (VException e) {
Log.d(TAG, "RPC failed, leaving current deck in place.");
@@ -164,27 +180,24 @@
*/
public static class Server implements ParticipantServer {
private static final String TAG = "ParticipantServer";
- private final Deck mDeck;
+ private final Presentation mPresentation;
- public Server(Deck d) {
- mDeck = d;
+ public Server(
+ VDeck d, String deckId, VPerson user,
+ String syncGroupName, String presentationId) {
+ Presentation p = new Presentation();
+ p.setDeck(d);
+ p.setDeckId(deckId);
+ p.setPerson(user);
+ p.setSyncgroupName(syncGroupName);
+ p.setPresentationId(presentationId);
+ mPresentation = p;
}
- public VDeck get(VContext ctx, ServerCall call)
+ public Presentation get(VContext ctx, ServerCall call)
throws VException {
Log.d(TAG, "Responding to Get RPC.");
- Log.d(TAG, " Sending mDeck = " + mDeck);
- VDeck d = new VDeck();
- d.setTitle(mDeck.getTitle());
- if (mDeck.getThumb() == null) {
- Log.d(TAG, " The response deck has no thumb.");
- } else {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- Bitmap bitmap = mDeck.getThumb();
- bitmap.compress(Bitmap.CompressFormat.JPEG, 60, stream);
- d.setThumbnail(stream.toByteArray());
- }
- return d;
+ return mPresentation;
}
}
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerFake.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerFake.java
index 7db977e..0e94f5b 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerFake.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerFake.java
@@ -9,6 +9,7 @@
import java.util.HashSet;
import java.util.Set;
+import io.v.android.apps.syncslides.db.VPerson;
import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Participant;
@@ -29,16 +30,22 @@
if (mCounter >= 2 && mCounter <= 8) {
participants.add(
ParticipantPeer.makeWithKnownDeck(
- "Alice",
+ new VPerson(
+ "dev.v.io/u/liz.lemon@gmail.com/android/io.v" +
+ ".android.apps.syncslides",
+ "Liz Lemon"),
mDeckFactory.make(
"Kale - Just eat it.",
"deckByAlice")));
}
- // Bob has less to say than Alice.
+ // Jack has less to say than Liz.
if (mCounter >= 4 && mCounter <= 6) {
participants.add(
ParticipantPeer.makeWithKnownDeck(
- "Bob",
+ new VPerson(
+ "dev.v.io/u/jack.donaghy@gmail.com/android/io.v" +
+ ".android.apps.syncslides",
+ "Jack Donaghy"),
mDeckFactory.make(
"Java - Object deluge.",
"deckByBob")));
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/participant_service.vdl b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/participant_service.vdl
index 6b011c9..dd41728 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/participant_service.vdl
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/participant_service.vdl
@@ -8,6 +8,15 @@
"io/v/android/apps/syncslides/db"
)
+// Who's presenting, and what are they presenting?
+type Presentation struct {
+ Deck db.VDeck
+ Person db.VPerson
+ DeckId string
+ PresentationId string
+ SyncgroupName string
+}
+
type Participant interface {
- Get() (db.VDeck | error)
+ Get() (Presentation | error)
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java
index ee26d1f..465d74b 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java
@@ -29,7 +29,7 @@
/**
* If true, enable use of syncbase as the DB, else use a fake.
*/
- public static final boolean ENABLE = false;
+ public static final boolean ENABLE = true;
}
public static class MtDiscovery {
@@ -45,7 +45,7 @@
* Every v23 service will be mounted in the namespace with a name
* prefixed by this.
*/
- private static String LIVE_PRESENTATION_PREFIX = "liveDeck";
+ private static String LIVE_PRESENTATION_PREFIX = "happyDeck";
/**
* TODO(jregan): Assure legal mount name (remove blanks and such).
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
index 734d79e..cd51312 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
@@ -23,6 +23,7 @@
import java.util.concurrent.Executors;
import io.v.android.apps.syncslides.SignInActivity;
+import io.v.android.apps.syncslides.db.DB;
import io.v.android.libs.security.BlessingsManager;
import io.v.android.v23.V;
import io.v.android.v23.services.blessing.BlessingCreationException;
@@ -100,6 +101,7 @@
Log.d(TAG, "unpacking blessing");
Blessings blessings = unpackBlessings(androidCtx, resultCode, data);
Singleton.get().configurePrincipal(blessings);
+ DB.Singleton.get(androidCtx).init();
} catch (BlessingCreationException e) {
throw new IllegalStateException(e);
} catch (VException e) {
@@ -176,14 +178,14 @@
"Cannot get blessings without an activity to return to.");
}
// Get the signed-in user's email to generate the blessings from.
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(androidCtx);
- String userEmail = prefs.getString(SignInActivity.PREF_USER_ACCOUNT_NAME, "");
+ String userEmail = SignInActivity.getUserEmail(androidCtx);
activity.startActivityForResult(
BlessingService.newBlessingIntent(androidCtx, userEmail),
BLESSING_REQUEST);
return;
}
- asyncConfigurePrincipal(blessings);
+ configurePrincipal(blessings);
+ DB.Singleton.get(mAndroidCtx).init();
}
public void flushServerFromCache(String name) {
@@ -206,6 +208,13 @@
return mBlessings != null;
}
+ /**
+ * Returns the blessings for this process.
+ */
+ public Blessings getBlessings() {
+ return mBlessings;
+ }
+
private void configurePrincipal(final Blessings blessings) {
Log.d(TAG, "configurePrincipal: blessings=" +
(blessings == null ? "null" : blessings.toString()));
@@ -224,15 +233,6 @@
(mBlessings == null ? "NONE!" : mBlessings.toString()));
}
- private void asyncConfigurePrincipal(final Blessings blessings) {
- mExecutor.execute(new Runnable() {
- @Override
- public void run() {
- configurePrincipal(blessings);
- }
- });
- }
-
public void shutdown(Behavior behavior) {
Log.d(TAG, "Shutdown");
if (mAndroidCtx == null) {
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java
index 1dec64c..4e8d0f2 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java
@@ -7,8 +7,11 @@
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.util.Base64;
import android.util.Log;
+import java.io.ByteArrayOutputStream;
+
import io.v.android.apps.syncslides.R;
import io.v.android.apps.syncslides.db.VDeck;
@@ -58,6 +61,13 @@
id);
}
+ public VDeck make(Deck deck) {
+ VDeck vd = new VDeck();
+ vd.setTitle(deck.getTitle());
+ vd.setThumbnail(makeBytesFromBitmap(deck.getThumb()));
+ return vd;
+ }
+
public Deck make(String title, Bitmap thumb, String id) {
title = (title == null || title.isEmpty()) ? Unknown.TITLE : title;
thumb = (thumb == null) ? mDefaultThumb : thumb;
@@ -65,6 +75,16 @@
return new DeckImpl(title, thumb, id);
}
+ public static byte[] makeBytesFromBitmap(Bitmap thumb) {
+ if (thumb == null) {
+ return new byte[0];
+ }
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ thumb.compress(
+ Bitmap.CompressFormat.JPEG, 60 /* quality */, stream);
+ return stream.toByteArray();
+ }
+
public static class Singleton {
private static volatile DeckFactory instance;
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Participant.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Participant.java
index fe83e5c..c59f92e 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Participant.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Participant.java
@@ -4,6 +4,8 @@
package io.v.android.apps.syncslides.model;
+import io.v.android.apps.syncslides.db.VPerson;
+
/**
* Someone taking part in a presentation.
*
@@ -12,14 +14,18 @@
*/
public interface Participant {
- // Name of the user participating, intended to be visible to others. This
- // can be a colloquial name as opposed to a 'real' name or email address
- // extracted from a device or blessing.
- String getUserName();
+ // User presenting.
+ VPerson getUser();
// Deck the user may be presenting.
Deck getDeck();
+ // The presentation ID.
+ String getPresentationId();
+
+ // The syncgroup name.
+ String getSyncgroupName();
+
// Name of a service with participant information.
String getServiceName();
@@ -40,8 +46,15 @@
* Keys for Bundle fields.
*/
class B {
+ public static final String SYNCGROUP_NAME = "participant_syncgroup_name";
+ public static final String PRESENTATION_ID = "participant_pres_id";
public static final String PARTICIPANT_ROLE = "participant_role";
public static final String PARTICIPANT_SHOULD_ADV = "participant_is_advertising";
public static final String PARTICIPANT_SYNCED = "participant_synced";
}
+
+ class Unknown {
+ public static final String SYNCGROUP_NAME = "unknown_syncgroup";
+ public static final String PRESENTATION_ID = "unknown_pres_id";
+ }
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Question.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Question.java
index 36b9c66..0298477 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Question.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Question.java
@@ -4,30 +4,40 @@
package io.v.android.apps.syncslides.model;
+import android.os.Parcel;
+import android.os.Parcelable;
+
import org.joda.time.DateTime;
/**
* Represents a question asked by the audience.
*/
-public class Question {
+public class Question implements Parcelable {
private String mId;
- private String mFirstName;
- private String mLastName;
- private DateTime mTime;
+ private String mName;
+ /**
+ * Time at which the question was asked in ms since the epoch. Stored as
+ * a long rather than DateTime because it is easier to serialize this way.
+ */
+ private long mTime;
/**
* @param id a uuid
- * @param firstName the first name of the questioner
- * @param lastName the last name of the questioner
- * @param time the time at which the question was asked
+ * @param name the name of the questioner
+ * @param time the time at which the question was asked in ms since the epoch
*/
- public Question(String id, String firstName, String lastName, DateTime time) {
+ public Question(String id, String name, long time) {
mId = id;
- mFirstName = firstName;
- mLastName = lastName;
+ mName = name;
mTime = time;
}
+ private Question(Parcel source) {
+ mId = source.readString();
+ mName = source.readString();
+ mTime = source.readLong();
+ }
+
/**
* Returns the unique id for the question.
*/
@@ -36,24 +46,43 @@
}
/**
- * Returns the first name of the questioner.
+ * Returns the name of the questioner.
*/
- public String getFirstName() {
- return mFirstName;
+ public String getName() {
+ return mName;
}
/**
- * Returns the last name of the questioner.
- */
- public String getLastName() {
- return mLastName;
- }
-
- /**
* Returns the time at which the question was asked.
*/
public DateTime getTime() {
- return mTime;
+ return new DateTime(mTime);
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mId);
+ dest.writeString(mName);
+ dest.writeLong(mTime);
+ }
+
+ public static final Parcelable.Creator<Question> CREATOR =
+ new Parcelable.Creator<Question>() {
+
+ @Override
+ public Question createFromParcel(Parcel source) {
+ return new Question(source);
+ }
+
+ @Override
+ public Question[] newArray(int size) {
+ return new Question[size];
+ }
+ };
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Slide.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Slide.java
index ec0ec7e..ae1e29e 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Slide.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Slide.java
@@ -19,4 +19,9 @@
* Returns the slide notes.
*/
String getNotes();
+
+ /**
+ * Sets the slide notes.
+ */
+ void setNotes(String notes);
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/SlideImpl.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/SlideImpl.java
index 8baff6f..987b168 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/SlideImpl.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/SlideImpl.java
@@ -5,26 +5,33 @@
package io.v.android.apps.syncslides.model;
import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
/**
* Application impl of Slide.
*/
public class SlideImpl implements Slide {
- private final Bitmap mThumbnail;
- private final String mNotes;
+ private final byte[] mThumbnail;
+ private String mNotes;
- public SlideImpl(Bitmap thumbnail, String notes) {
+ public SlideImpl(byte[] thumbnail, String notes) {
mThumbnail = thumbnail;
mNotes = notes;
}
@Override
public Bitmap getImage() {
- return mThumbnail;
+ return BitmapFactory.decodeByteArray(
+ mThumbnail, 0 /* offset */, mThumbnail.length);
}
@Override
public String getNotes() {
return mNotes;
}
+
+ @Override
+ public void setNotes(String notes) {
+ mNotes = notes;
+ }
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/notify/NotifierNative.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/notify/NotifierNative.java
index 818933d..1f701ea 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/notify/NotifierNative.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/notify/NotifierNative.java
@@ -53,7 +53,7 @@
.setFullScreenIntent(contentIntent, false)
.setAutoCancel(true)
.setWhen(System.currentTimeMillis())
- .setContentTitle(p.getUserName())
+ .setContentTitle(p.getUser().getName())
.setContentText(p.getDeck().getTitle())
// TODO(jregan): Need a better icon.
.setSmallIcon(R.drawable.orange_circle)
diff --git a/projects/syncslides/app/src/main/res/layout/deck_card.xml b/projects/syncslides/app/src/main/res/layout/deck_card.xml
index 72ecd2c..66302b8 100644
--- a/projects/syncslides/app/src/main/res/layout/deck_card.xml
+++ b/projects/syncslides/app/src/main/res/layout/deck_card.xml
@@ -16,7 +16,7 @@
<ImageView
android:id="@+id/deck_thumb"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/deck_thumb_height"
android:scaleType="centerCrop"/>
<!-- Display the title of the deck and a menu to configure it. -->
diff --git a/projects/syncslides/app/src/main/res/mipmap-hdpi/vanadium_launcher.png b/projects/syncslides/app/src/main/res/mipmap-hdpi/vanadium_launcher.png
new file mode 100644
index 0000000..9d5e0d6
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/mipmap-hdpi/vanadium_launcher.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/mipmap-mdpi/vanadium_launcher.png b/projects/syncslides/app/src/main/res/mipmap-mdpi/vanadium_launcher.png
new file mode 100644
index 0000000..66f1514
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/mipmap-mdpi/vanadium_launcher.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/mipmap-xhdpi/vanadium_launcher.png b/projects/syncslides/app/src/main/res/mipmap-xhdpi/vanadium_launcher.png
new file mode 100644
index 0000000..9dfa8d7
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/mipmap-xhdpi/vanadium_launcher.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/mipmap-xxhdpi/vanadium_launcher.png b/projects/syncslides/app/src/main/res/mipmap-xxhdpi/vanadium_launcher.png
new file mode 100644
index 0000000..3c7d066
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/mipmap-xxhdpi/vanadium_launcher.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/values/dimens.xml b/projects/syncslides/app/src/main/res/values/dimens.xml
index a5f280c..03925e3 100644
--- a/projects/syncslides/app/src/main/res/values/dimens.xml
+++ b/projects/syncslides/app/src/main/res/values/dimens.xml
@@ -10,8 +10,9 @@
<dimen name="fab_margin">16dp</dimen>
<dimen name="fab_elevation">4dp</dimen>
- <dimen name="deck_card_width">150dp</dimen>
+ <dimen name="deck_card_width">200dp</dimen>
<dimen name="deck_card_margin">5dp</dimen>
+ <dimen name="deck_thumb_height">115dp</dimen>
<dimen name="card_toolbar_height">60dp</dimen>
<dimen name="toolbar_and_statusbar_height">80dp</dimen>
<dimen name="toolbar_inset">10dp</dimen>
diff --git a/projects/syncslides/app/src/main/res/values/strings.xml b/projects/syncslides/app/src/main/res/values/strings.xml
index 48013cd..c6af291 100644
--- a/projects/syncslides/app/src/main/res/values/strings.xml
+++ b/projects/syncslides/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
<resources>
- <string name="app_name">aaaSyncSlides</string>
+ <string name="app_name">SyncSlides</string>
<string name="title_account1">Account 1</string>
<string name="title_account2">Account 2</string>