Merge branch 'master' into ble
Change-Id: I7083bedd33abf32e18e965bf7b7f4c6da08655fe
diff --git a/lib/src/main/java/io/v/v23/V.java b/lib/src/main/java/io/v/v23/V.java
index 0b00a45..464825e 100644
--- a/lib/src/main/java/io/v/v23/V.java
+++ b/lib/src/main/java/io/v/v23/V.java
@@ -52,6 +52,14 @@
private static volatile VRuntime runtime = null;
private static volatile boolean initOnceDone = false;
+ private static boolean isDarwin() {
+ return System.getProperty("os.name").toLowerCase().contains("os x");
+ }
+
+ private static boolean isLinux() {
+ return System.getProperty("os.name").toLowerCase().contains("linux");
+ }
+
private static synchronized void initOnce() {
if (initOnceDone) {
return;
@@ -64,8 +72,19 @@
// Thrown if the library does not exist. In this case, try to find it in our classpath.
errors.add(new RuntimeException("loadLibrary attempt failed", ule));
try {
- URL resource = Resources.getResource("libv23.so");
- File file = File.createTempFile("libv23-", ".so");
+ URL resource = null;
+ File file = null;
+ if (isLinux()) {
+ resource = Resources.getResource("libv23.so");
+ file = File.createTempFile("libv23-", ".so");
+ } else if (isDarwin()) {
+ resource = Resources.getResource("libv23.dylib");
+ file = File.createTempFile("libv23-", ".dylib");
+ } else {
+ String os = System.getProperty("os.name");
+ errors.add(new RuntimeException("unsupported OS: " + os));
+ throw new RuntimeException("Unsupported OS: " + os, new VLoaderException(errors));
+ }
file.deleteOnExit();
ByteStreams.copy(resource.openStream(), new FileOutputStream(file));
System.load(file.getAbsolutePath());
diff --git a/projects/syncslidepresenter/build.gradle b/projects/syncslidepresenter/build.gradle
new file mode 100644
index 0000000..e13c1f1
--- /dev/null
+++ b/projects/syncslidepresenter/build.gradle
@@ -0,0 +1,44 @@
+apply plugin: 'application'
+apply plugin: 'java'
+apply plugin: 'io.v.vdl'
+
+buildscript {
+ repositories {
+ maven {
+ url 'https://maven.v.io'
+ }
+ }
+ dependencies {
+ classpath 'io.v:gradle-plugin:0.1-SNAPSHOT'
+ }
+}
+
+mainClassName = "io.v.syncslidepresenter"
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ compile project(':lib')
+ compile 'com.google.guava:guava:18'
+ compile 'com.beust:jcommander:1.48'
+}
+
+task copyLib(type: Copy) {
+ 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))
+}
+
+clean {
+ delete 'src/main/resources/libv23.so'
+ delete 'src/main/resources/libv23.dylib'
+}
+
+vdl {
+ inputPaths += [project(':projects:syncslides').projectDir.absolutePath + '/app/src/main/java']
+}
+
+sourceCompatibility = '1.8'
+targetCompatibility = '1.8'
\ No newline at end of file
diff --git a/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java b/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
new file mode 100644
index 0000000..4f23fa5
--- /dev/null
+++ b/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
@@ -0,0 +1,304 @@
+// 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.syncslidepresenter;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.joda.time.Duration;
+
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Image;
+import java.awt.Window;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.WindowConstants;
+
+import io.v.android.apps.syncslides.db.VCurrentSlide;
+import io.v.android.apps.syncslides.db.VSlide;
+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.naming.Endpoint;
+import io.v.v23.rpc.Server;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.access.AccessList;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.services.syncbase.nosql.KeyValue;
+import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
+import io.v.v23.services.watch.ResumeMarker;
+import io.v.v23.syncbase.Syncbase;
+import io.v.v23.syncbase.SyncbaseApp;
+import io.v.v23.syncbase.SyncbaseService;
+import io.v.v23.syncbase.nosql.BatchDatabase;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.RowRange;
+import io.v.v23.syncbase.nosql.Stream;
+import io.v.v23.syncbase.nosql.Syncgroup;
+import io.v.v23.syncbase.nosql.Table;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+
+/**
+ * The entry point for syncslidepresenter.
+ */
+public class Main {
+ private static final Logger logger = Logger.getLogger(Main.class.getName());
+ private static final String SYNCBASE_APP = "syncslides";
+ private static final String SYNCBASE_DB = "syncslides";
+ private static final String PRESENTATIONS_TABLE = "Presentations";
+ private static final String DECKS_TABLE = "Decks";
+ private final Table presentations;
+ private final Table decks;
+ private final ImageViewer viewer;
+
+ private Database db;
+
+ private VContext context;
+
+ public static void main(String[] args) throws SyncbaseServer.StartException, VException, IOException {
+ Options options = new Options();
+ JCommander commander = new JCommander(options);
+ try {
+ commander.parse(args);
+ } catch (ParameterException e) {
+ logger.warning("Could not parse parameters: " + e.getMessage());
+ commander.usage();
+ return;
+ }
+
+ if (options.help) {
+ commander.usage();
+ return;
+ }
+
+ // Make command-Q do the same as closing the main frame (i.e. exit).
+ System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
+
+ JFrame frame = new JFrame();
+ enableOSXFullscreen(frame);
+ frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ frame.setVisible(true);
+
+ VContext baseContext = V.init();
+
+ AccessList acl = new AccessList(ImmutableList.of(new BlessingPattern("...")),
+ ImmutableList.<String>of());
+ 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);
+ if (server.getStatus().getEndpoints().length > 0) {
+ logger.info("Mounted syncbase server at the following endpoints: ");
+ for (Endpoint e : server.getStatus().getEndpoints()) {
+ logger.info("\t" + e);
+ }
+ logger.info("End of endpoint list");
+
+ SyncbaseService service
+ = Syncbase.newService("/" + server.getStatus().getEndpoints()[0]);
+ SyncbaseApp app = service.getApp(SYNCBASE_APP);
+ if (!app.exists(baseContext)) {
+ app.create(baseContext, permissions);
+ }
+ Database db = app.getNoSqlDatabase(SYNCBASE_DB, null);
+ if (!db.exists(baseContext)) {
+ db.create(baseContext, permissions);
+ }
+ Table decks = db.getTable(DECKS_TABLE);
+ if (!decks.exists(baseContext)) {
+ decks.create(baseContext, permissions);
+ }
+ Table presentations = db.getTable(PRESENTATIONS_TABLE);
+ if (!presentations.exists(baseContext)) {
+ 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, presentationPanel, db, decks, presentations);
+ m.joinPresentation(options.presentationName, options.joinTimeoutSeconds,
+ options.currentSlideKey, options.slideRowFormat);
+ }
+ }
+
+ public Main(VContext context, ImageViewer viewer, Database db, Table decks,
+ Table presentations) throws VException {
+ this.context = context;
+ this.db = db;
+ this.presentations = presentations;
+ this.decks = decks;
+ this.viewer = viewer;
+ }
+
+ public void joinPresentation(final String syncgroupName,
+ int joinTimeoutSeconds,
+ String currentSlideKey,
+ String slideRowFormat) throws VException {
+ Syncgroup syncgroup = db.getSyncgroup(syncgroupName);
+ syncgroup.join(context.withTimeout(Duration.standardSeconds(joinTimeoutSeconds)),
+ new SyncgroupMemberInfo((byte) 1));
+ for (String member : syncgroup.getMembers(context).keySet()) {
+ logger.info("Member: " + member);
+ }
+
+ for (KeyValue keyValue : presentations.scan(context, RowRange.prefix(""))) {
+ System.out.println("Presentation: " + keyValue);
+ }
+ BatchDatabase batch = db.beginBatch(context, null);
+ ResumeMarker marker = batch.getResumeMarker(context);
+ Stream<WatchChange> watchStream = db.watch(context, presentations.name(), currentSlideKey,
+ marker);
+
+ for (WatchChange w : watchStream) {
+ logger.info("Change detected in " + w.getRowName());
+ logger.info("Type: " + w.getChangeType());
+ try {
+ VCurrentSlide currentSlide = (VCurrentSlide) VomUtil.decode(w.getVomValue(),
+ 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);
+ } catch (IOException | VException e) {
+ logger.log(Level.WARNING, "exception encountered while handling change event", e);
+ }
+ }
+ }
+
+ private static void enableOSXFullscreen(Window window) {
+ Preconditions.checkNotNull(window);
+ try {
+ // This class may not be present on the system (e.g. if we're not on MacOSX),
+ // use reflection so that we can make this an optional dependency.
+ Class util = Class.forName("com.apple.eawt.FullScreenUtilities");
+ Class params[] = new Class[]{Window.class, Boolean.TYPE};
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Method method = util.getMethod("setWindowCanFullScreen", params);
+ method.invoke(util, window, true);
+ } catch (ClassNotFoundException e) {
+ // Probably not on Mac OS X
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Couldn't enable fullscreen on Mac OS X", e);
+ }
+ }
+
+ public static class Options {
+ @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";
+
+ @Parameter(names = {"-h", "--help"}, description = "display this help message", help = true)
+ private boolean help = false;
+ }
+
+ private static class ScaleToFitJPanel extends JPanel implements ImageViewer {
+ private Image image;
+
+ public ScaleToFitJPanel() {
+ super();
+ setPreferredSize(new Dimension(250, 250));
+ addComponentListener(new ComponentAdapter() {
+ @Override
+ public void componentResized(ComponentEvent e) {
+ repaint();
+ }
+ });
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+
+ if (image != null) {
+ int width;
+ int height;
+ double containerRatio = 1.0d * getWidth() / getHeight();
+ double imageRatio = 1.0d * image.getWidth(null) / image.getHeight(null);
+
+ if (containerRatio < imageRatio) {
+ width = getWidth();
+ height = (int) (getWidth() / imageRatio);
+ } else {
+ width = (int) (getHeight() * imageRatio);
+ height = getHeight();
+ }
+
+ // Center the image in the container.
+ int x = (int) (((double) getWidth() / 2) - ((double) width / 2));
+ int y = (int) (((double) getHeight()/ 2) - ((double) height / 2));
+
+ g.drawImage(image, x, y, width, height, this);
+ }
+ }
+
+ @Override
+ public void setImage(Image image) {
+ this.image = image;
+ setPreferredSize(new Dimension(image.getWidth(null), image.getHeight(null)));
+ repaint();
+ }
+ }
+
+ private interface ImageViewer {
+ void setImage(Image image);
+ }
+}
diff --git a/projects/syncslides/app/build.gradle b/projects/syncslides/app/build.gradle
index 81966f8..c8c0063 100644
--- a/projects/syncslides/app/build.gradle
+++ b/projects/syncslides/app/build.gradle
@@ -48,7 +48,7 @@
defaultConfig {
applicationId "io.v.android.apps.syncslides"
- minSdkVersion 21
+ minSdkVersion 22
targetSdkVersion 23
versionCode 1
versionName "1.0"
diff --git a/projects/syncslides/app/src/main/AndroidManifest.xml b/projects/syncslides/app/src/main/AndroidManifest.xml
index 1719535..6030afe 100644
--- a/projects/syncslides/app/src/main/AndroidManifest.xml
+++ b/projects/syncslides/app/src/main/AndroidManifest.xml
@@ -1,29 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
-<manifest package="io.v.android.apps.syncslides"
- xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.v.android.apps.syncslides" >
+
+ <uses-sdk android:minSdkVersion="22" />
+
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.USE_CREDENTIALS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:theme="@style/AppTheme">
+ android:theme="@style/AppTheme" >
<activity
- android:name=".DeckChooserActivity"
- android:label="@string/app_name">
+ android:name=".SignInActivity"
+ android:label="@string/app_name" >
<intent-filter>
- <action android:name="android.intent.action.MAIN"/>
-
- <category android:name="android.intent.category.LAUNCHER"/>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <activity android:name=".PresentationActivity"/>
+ <activity
+ android:name=".DeckChooserActivity"
+ android:label="@string/app_name" >
+ </activity>
+ <activity android:name=".PresentationActivity" />
<service
android:name=".discovery.ParticipantPeer"
android:exported="false"
android:label="Location Service"
- android:process=":ParticipantPeer">
+ android:process=":ParticipantPeer" >
</service>
</application>
- <uses-permission android:name="android.permission.READ_PHONE_STATE" />
</manifest>
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 2790330..36304cb 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
@@ -5,25 +5,24 @@
package io.v.android.apps.syncslides;
import android.content.Intent;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v4.app.FragmentManager;
import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
-import android.support.v4.widget.DrawerLayout;
import io.v.android.apps.syncslides.db.DB;
-import io.v.android.apps.syncslides.discovery.V23Manager;
-import io.v.android.v23.services.blessing.BlessingCreationException;
-import io.v.v23.security.Blessings;
-import io.v.v23.verror.VException;
+import io.v.android.apps.syncslides.misc.V23Manager;
+import io.v.android.apps.syncslides.model.DeckFactory;
public class DeckChooserActivity extends AppCompatActivity
implements NavigationDrawerFragment.NavigationDrawerCallbacks {
private static final String TAG = "DeckChooser";
/**
- * Fragment managing the behaviors, interactions and deck of the navigation drawer.
+ * Fragment managing the behaviors, interactions and deck of the navigation
+ * drawer.
*/
private NavigationDrawerFragment mNavigationDrawerFragment;
private DB mDB;
@@ -32,6 +31,8 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
+ // Initialize the DeckFactory.
+ DeckFactory.Singleton.get(getApplicationContext());
// Immediately initialize V23, possibly sending user to the
// AccountManager to get blessings.
V23Manager.Singleton.get().init(getApplicationContext(), this);
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 19ebbfd..6c25a93 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
@@ -36,7 +36,7 @@
import io.v.android.apps.syncslides.db.DB;
import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Slide;
import io.v.android.apps.syncslides.model.SlideImpl;
@@ -77,7 +77,6 @@
onImportDeck();
}
});
-
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.deck_grid);
mRecyclerView.setHasFixedSize(true);
@@ -208,7 +207,7 @@
String id = UUID.randomUUID().toString();
String title = metadata.getString("Title");
Bitmap thumb = readImage(dir, metadata.getString("Thumb"));
- Deck deck = new DeckImpl(title, thumb, id);
+ Deck deck = DeckFactory.Singleton.get().make(title, thumb, id);
Slide[] slides = readSlides(dir, metadata);
// TODO(spetrovic): Do this asynchronously.
DB.Singleton.get(getActivity().getApplicationContext()).importDeck(deck, slides);
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 3555157..75fbe0b 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
@@ -2,6 +2,7 @@
// 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;
+
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -15,11 +16,13 @@
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toolbar;
+
import io.v.android.apps.syncslides.db.DB;
import io.v.android.apps.syncslides.discovery.DiscoveryManager;
import io.v.android.apps.syncslides.model.Deck;
import io.v.android.apps.syncslides.model.Listener;
import io.v.android.apps.syncslides.model.Participant;
+import io.v.android.apps.syncslides.model.Role;
/**
* Provides a list of decks to be shown in the RecyclerView of the
@@ -41,7 +44,7 @@
throw new IllegalStateException("Wrong lifecycle.");
}
Log.d(TAG, "Starting.");
- DiscoveryManager dm = DiscoveryManager.make();
+ DiscoveryManager dm = DiscoveryManager.make(context);
// Listening stops below in mLiveDecks.discard.
dm.setListener(this);
dm.start(context);
@@ -52,16 +55,19 @@
public void notifyItemChanged(int position) {
DeckListAdapter.this.notifyItemChanged(mLiveDecks.getItemCount() + position);
}
+
@Override
public void notifyItemInserted(int position) {
DeckListAdapter.this.notifyItemInserted(mLiveDecks.getItemCount() + position);
}
+
@Override
public void notifyItemRemoved(int position) {
DeckListAdapter.this.notifyItemRemoved(mLiveDecks.getItemCount() + position);
}
});
}
+
/**
* Stops any background monitoring of the underlying data.
*/
@@ -72,20 +78,22 @@
mDecks.discard();
mDecks = null;
}
+
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.deck_card, parent, false);
return new ViewHolder(v);
}
+
@Override
- public void onBindViewHolder(final ViewHolder holder, int i) {
+ public void onBindViewHolder(final ViewHolder holder, int deckIndex) {
final Deck deck;
final Role role;
// If the position is less than the number of live presentation decks, get deck card from
// there (and don't allow the user to delete the deck). If not, get the card from the DB.
- if (i < mLiveDecks.getItemCount()) {
- deck = mLiveDecks.get(i);
+ if (deckIndex < mLiveDecks.getItemCount()) {
+ deck = mLiveDecks.get(deckIndex);
holder.mToolbarLiveNow
.setText(holder.itemView.getResources().getString(R.string.presentation_live));
holder.mToolbarLiveNow.setVisibility(View.VISIBLE);
@@ -93,7 +101,7 @@
holder.mToolbar.getMenu().clear();
role = Role.AUDIENCE;
} else {
- deck = mDecks.get(i - mLiveDecks.getItemCount());
+ deck = mDecks.get(deckIndex - mLiveDecks.getItemCount());
// TODO(afergan): Set actual date here.
holder.mToolbarLastOpened.setText("Opened on Oct 26, 2015");
holder.mToolbarLastOpened.setVisibility(View.VISIBLE);
@@ -128,28 +136,28 @@
String deviceId = "355499060490393"; // Nexus 6 ZX1G22MLNL
DB.Singleton.get(context).joinPresentation(
// TODO(kash): Use the real syncgroup name.
- "/192.168.86.254:8101/"+deviceId+"/%%sync/syncslides/" +
+ "/192.168.86.254:8101/" + deviceId + "/%%sync/syncslides/" +
"deckId1/randomPresentationId1",
new DB.Callback<Void>() {
@Override
public void done(Void aVoid) {
- // Intent for the activity to open when user selects the thumbnail.
- Intent intent = new Intent(context, PresentationActivity.class);
- intent.putExtras(deck.toBundle(null));
- intent.putExtra(Participant.B.PARTICIPANT_ROLE, role);
- context.startActivity(intent);
+ showSlides(context, deck, role);
}
});
} else {
- // Intent for the activity to open when user selects the thumbnail.
- Intent intent = new Intent(context, PresentationActivity.class);
- intent.putExtras(deck.toBundle(null));
- intent.putExtra(Participant.B.PARTICIPANT_ROLE, role);
- context.startActivity(intent);
+ showSlides(context, deck, role);
}
}
});
}
+
+ private void showSlides(Context context, Deck deck, Role role) {
+ Intent intent = new Intent(context, PresentationActivity.class);
+ intent.putExtra(Deck.B.DECK_ID, deck.getId());
+ intent.putExtra(Participant.B.PARTICIPANT_ROLE, role);
+ context.startActivity(intent);
+ }
+
@Override
public int getItemCount() {
return mLiveDecks.getItemCount() + mDecks.getItemCount();
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/FullscreenSlideFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/FullscreenSlideFragment.java
index 2676ca5..db21323 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/FullscreenSlideFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/FullscreenSlideFragment.java
@@ -14,6 +14,7 @@
import java.util.List;
import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.model.Role;
import io.v.android.apps.syncslides.model.Slide;
public class FullscreenSlideFragment extends Fragment {
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 b12c4ce..46dfc86 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
@@ -31,6 +31,7 @@
import io.v.android.apps.syncslides.db.DB;
import io.v.android.apps.syncslides.model.Question;
+import io.v.android.apps.syncslides.model.Role;
import io.v.android.apps.syncslides.model.Slide;
/**
@@ -127,16 +128,18 @@
mFabSync.setVisibility(View.VISIBLE);
}
- mFabSync.setOnClickListener(new View.OnClickListener() {
+ mFabSync.setOnClickListener(new NavigateClickListener() {
@Override
public void onClick(View v) {
+ super.onClick(v);
sync();
mFabSync.setVisibility(View.INVISIBLE);
}
});
- View.OnClickListener previousSlideListener = new View.OnClickListener() {
+ View.OnClickListener previousSlideListener = new NavigateClickListener() {
@Override
public void onClick(View v) {
+ super.onClick(v);
previousSlide();
}
};
@@ -145,9 +148,10 @@
mPrevThumb = (ImageView) rootView.findViewById(R.id.prev_thumb);
mPrevThumb.setOnClickListener(previousSlideListener);
- View.OnClickListener nextSlideListener = new View.OnClickListener() {
+ View.OnClickListener nextSlideListener = new NavigateClickListener() {
@Override
public void onClick(View v) {
+ super.onClick(v);
nextSlide();
}
};
@@ -165,16 +169,18 @@
mNextThumb.setOnClickListener(nextSlideListener);
mQuestions = (ImageView) rootView.findViewById(R.id.questions);
// TODO(kash): Hide the mQuestions button if mRole == BROWSER.
- mQuestions.setOnClickListener(new View.OnClickListener() {
+ mQuestions.setOnClickListener(new NavigateClickListener() {
@Override
public void onClick(View v) {
+ super.onClick(v);
questionButton();
}
});
mCurrentSlide = (ImageView) rootView.findViewById(R.id.slide_current_medium);
- mCurrentSlide.setOnClickListener(new View.OnClickListener() {
+ mCurrentSlide.setOnClickListener(new NavigateClickListener() {
@Override
public void onClick(View v) {
+ super.onClick(v);
if (mRole == Role.AUDIENCE || mRole == Role.BROWSER) {
((PresentationActivity) getActivity()).showFullscreenSlide(mUserSlideNum);
}
@@ -192,6 +198,7 @@
unsync();
}
});
+
// The parent of mNotes needs to be focusable in order to clear focus
// from mNotes when done editing. We set the attributes in code rather
// than in XML because it is too easy to add an extra level of layout
@@ -202,9 +209,10 @@
parent.setFocusableInTouchMode(true);
View slideListIcon = rootView.findViewById(R.id.slide_list);
- slideListIcon.setOnClickListener(new View.OnClickListener() {
+ slideListIcon.setOnClickListener(new NavigateClickListener() {
@Override
public void onClick(View v) {
+ super.onClick(v);
if (mRole == Role.AUDIENCE) {
((PresentationActivity) getActivity()).showSlideList();
} else {
@@ -298,20 +306,26 @@
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_save:
- Toast.makeText(getContext(), "Saving notes", Toast.LENGTH_SHORT).show();
- mNotes.clearFocus();
- InputMethodManager inputManager =
- (InputMethodManager) getContext().
- getSystemService(Context.INPUT_METHOD_SERVICE);
- inputManager.hideSoftInputFromWindow(
- getActivity().getCurrentFocus().getWindowToken(),
- InputMethodManager.HIDE_NOT_ALWAYS);
- ((PresentationActivity) getActivity()).setUiImmersive(true);
+ saveNotes();
return true;
}
return false;
}
+ 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);
+ inputManager.hideSoftInputFromWindow(
+ getActivity().getCurrentFocus().getWindowToken(),
+ InputMethodManager.HIDE_NOT_ALWAYS);
+ ((PresentationActivity) getActivity()).setUiImmersive(true);
+ }
+ }
+
private void unsync() {
if (mRole == Role.AUDIENCE && mSynced) {
mSynced = false;
@@ -456,9 +470,10 @@
*/
private void handoffControl() {
//TODO(afergan): Change slide presenter to the audience member at mQuestionerPosition.
- View.OnClickListener snackbarClickListener = new View.OnClickListener() {
+ View.OnClickListener snackbarClickListener = new NavigateClickListener() {
@Override
public void onClick(View v) {
+ super.onClick(v);
//TODO(afergan): End handoff, presenter regains control of presentation.
}
};
@@ -504,4 +519,11 @@
thumbParams.height = (int) ((9 / 16.0) * grandparent.getMeasuredWidth());
}
}
-}
+
+ public class NavigateClickListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ saveNotes();
+ }
+ }
+}
\ No newline at end of file
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 61fc9b4..2f5d437 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
@@ -15,6 +15,7 @@
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceManager;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -24,6 +25,10 @@
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
/**
* Fragment used for managing interactions for and presentation of a navigation drawer.
@@ -31,6 +36,7 @@
* design guidelines</a> for a complete explanation of the behaviors implemented here.
*/
public class NavigationDrawerFragment extends Fragment {
+ private static final String TAG = "NavigationDrawer";
/**
* Remember the position of the selected item.
@@ -56,6 +62,7 @@
private DrawerLayout mDrawerLayout;
private ListView mDrawerListView;
private View mFragmentContainerView;
+ private JSONObject mUserProfile;
private int mCurrentSelectedPosition = 0;
private boolean mFromSavedInstanceState;
@@ -70,8 +77,14 @@
// Read in the flag indicating whether or not the user has demonstrated awareness of the
// drawer. See PREF_USER_LEARNED_DRAWER for details.
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
- mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);
+ 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);
+ }
if (savedInstanceState != null) {
mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
@@ -100,15 +113,33 @@
selectItem(position);
}
});
- mDrawerListView.setAdapter(new ArrayAdapter<String>(
+ mDrawerListView.setAdapter(new ArrayAdapter<JSONObject>(
getActionBar().getThemedContext(),
- android.R.layout.simple_list_item_activated_1,
+ android.R.layout.simple_list_item_activated_2,
android.R.id.text1,
- new String[]{
- getString(R.string.title_account1),
- getString(R.string.title_account2),
- getString(R.string.title_account3),
- }));
+ new JSONObject[]{mUserProfile}) {
+ @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);
+ return view;
+ }
+ });
mDrawerListView.setItemChecked(mCurrentSelectedPosition, true);
return mDrawerListView;
}
@@ -269,7 +300,7 @@
/**
* Callbacks interface that all activities using this fragment must implement.
*/
- public static interface NavigationDrawerCallbacks {
+ public interface NavigationDrawerCallbacks {
/**
* Called when an item in the navigation drawer is selected.
*/
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 48fafc0..9ff1fa9 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
@@ -13,11 +13,13 @@
import android.widget.Toast;
import io.v.android.apps.syncslides.db.DB;
-import io.v.android.apps.syncslides.discovery.ParticipantServerImpl;
-import io.v.android.apps.syncslides.discovery.V23Manager;
+import io.v.android.apps.syncslides.discovery.ParticipantPeer;
+import io.v.android.apps.syncslides.misc.Config;
+import io.v.android.apps.syncslides.misc.V23Manager;
import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Participant;
+import io.v.android.apps.syncslides.model.Role;
public class PresentationActivity extends AppCompatActivity {
private static final String TAG = "PresentationActivity";
@@ -32,7 +34,7 @@
*/
private Role mRole;
// TODO(kash): Replace this with the presentation id.
- private String mPresentationId = "randomPresentation1";
+ private String mPresentationId = "randomPresentationId1";
private boolean mSynced;
/**
@@ -49,6 +51,8 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
+ // Initialize the DeckFactory.
+ DeckFactory.Singleton.get(getApplicationContext());
// Immediately initialize V23, possibly sending user to the
// AccountManager to get blessings.
V23Manager.Singleton.get().init(getApplicationContext(), this);
@@ -57,16 +61,17 @@
mShouldBeAdvertising = false;
mIsAdvertising = false;
+ String deckId;
if (savedInstanceState == null) {
Log.d(TAG, "savedInstanceState is null");
- mDeck = DeckImpl.fromBundle(getIntent().getExtras());
+ deckId = getIntent().getStringExtra(Deck.B.DECK_ID);
mRole = (Role) getIntent().getSerializableExtra(
Participant.B.PARTICIPANT_ROLE);
mSynced = true;
} else {
Log.d(TAG, "savedInstanceState is NOT null");
- mDeck = DeckImpl.fromBundle(savedInstanceState);
mRole = (Role) savedInstanceState.get(Participant.B.PARTICIPANT_ROLE);
+ deckId = savedInstanceState.getString(Deck.B.DECK_ID);
mSynced = savedInstanceState.getBoolean(Participant.B.PARTICIPANT_SYNCED);
mShouldBeAdvertising = savedInstanceState.getBoolean(Participant.B.PARTICIPANT_SHOULD_ADV);
if (mShouldBeAdvertising) {
@@ -74,6 +79,13 @@
}
}
+ Log.d(TAG, "Role = " + mRole);
+ mDeck = DB.Singleton.get(getApplicationContext()).getDeck(deckId);
+ if (mDeck == null) {
+ throw new IllegalArgumentException("Unusable deckId: "+ deckId);
+ }
+ Log.d(TAG, "Using deck: " + mDeck);
+
// TODO(jregan): This appears to be an attempt to avoid fragment
// re-inflation, possibly the right thing to do is move the code
// below to another flow step, e.g. onRestoreInstanceState.
@@ -81,7 +93,7 @@
return;
}
- if (mShouldBeAdvertising){
+ if (mShouldBeAdvertising) {
startAdvertising();
}
@@ -109,10 +121,10 @@
@Override
protected void onSaveInstanceState(Bundle b) {
- Log.d(TAG, "onSaveInstanceState");
super.onSaveInstanceState(b);
- mDeck.toBundle(b);
+ Log.d(TAG, "onSaveInstanceState");
b.putSerializable(Participant.B.PARTICIPANT_ROLE, mRole);
+ b.putString(Deck.B.DECK_ID, mDeck.getId());
b.putBoolean(Participant.B.PARTICIPANT_SYNCED, mSynced);
b.putBoolean(Participant.B.PARTICIPANT_SHOULD_ADV, mShouldBeAdvertising);
}
@@ -121,7 +133,7 @@
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
- if (mShouldBeAdvertising){
+ if (mShouldBeAdvertising) {
startAdvertising();
}
}
@@ -157,7 +169,7 @@
}
private boolean shouldUseV23() {
- return Participant.ENABLE_MT_DISCOVERY && V23Manager.Singleton.get().isBlessed();
+ return Config.MtDiscovery.ENABLE && V23Manager.Singleton.get().isBlessed();
}
private void startAdvertising() {
@@ -169,8 +181,8 @@
}
if (shouldUseV23()) {
V23Manager.Singleton.get().mount(
- Participant.Mt.makeMountName(mDeck),
- new ParticipantServerImpl(mDeck));
+ Config.MtDiscovery.makeMountName(mDeck),
+ new ParticipantPeer.Server(mDeck));
Log.d(TAG, "MT advertising started.");
} else {
Log.d(TAG, "No means to start advertising.");
@@ -211,7 +223,7 @@
}
});
mRole = Role.PRESENTER;
- showNavigateFragment(0);
+ showNavigateFragmentWithBackStack(0);
}
/**
@@ -240,8 +252,8 @@
}
/**
- * Shows the navigate fragment where the user can see the current slide
- * and navigate to other components of the slide presentation. This version
+ * 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.
*
@@ -268,8 +280,8 @@
}
/**
- * Return if the device is synced with the presenter (true if the device
- * is the presenter).
+ * Return if the device is synced with the presenter (true if the device is
+ * the presenter).
*/
public boolean getSynced() {
return mSynced;
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 1a4a9c5..539e081 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
@@ -43,6 +43,12 @@
return builder.create();
}
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ ((PresentationActivity) getActivity()).setUiImmersive(true);
+ }
+
// Send back the position of the questioner to the NavigateFragment.
private void sendResult(int position) {
Intent intent = new 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
new file mode 100644
index 0000000..3d225bc
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
@@ -0,0 +1,234 @@
+// 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;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Signs in the user into one of his Gmail accounts.
+ */
+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 int REQUEST_CODE_PICK_ACCOUNT = 1000;
+ private static final int REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL = 1001;
+
+ private static final String OAUTH_PROFILE = "email";
+ private static final String OAUTH_SCOPE = "oauth2:" + OAUTH_PROFILE;
+ private static final String OAUTH_USERINFO_URL =
+ "https://www.googleapis.com/oauth2/v2/userinfo";
+
+ private SharedPreferences mPrefs;
+ private String mAccountName;
+ private ProgressDialog mProgressDialog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_sign_in);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mAccountName = mPrefs.getString(SignInActivity.PREF_USER_ACCOUNT_NAME, "");
+ mProgressDialog = new ProgressDialog(this);
+ if (mAccountName.isEmpty()) {
+ mProgressDialog.setMessage("Signing in...");
+ mProgressDialog.show();
+ pickAccount();
+ } else {
+ finishActivity();
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CODE_PICK_ACCOUNT: {
+ if (resultCode != RESULT_OK) {
+ Toast.makeText(this, "Must pick account", Toast.LENGTH_LONG).show();
+ pickAccount();
+ break;
+ }
+ pickAccountDone(data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME));
+ break;
+ }
+ case REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL:
+ if (resultCode != RESULT_OK) {
+ Log.e(TAG, "User didn't approve oauth2 request");
+ break;
+ }
+ fetchUserProfile();
+ break;
+ }
+ }
+
+ private void pickAccount() {
+ Intent chooseIntent = AccountManager.newChooseAccountIntent(
+ null, null, new String[]{"com.google"}, false, null, null, null, null);
+ startActivityForResult(chooseIntent, REQUEST_CODE_PICK_ACCOUNT);
+ }
+
+ private void pickAccountDone(String accountName) {
+ mAccountName = accountName;
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putString(PREF_USER_ACCOUNT_NAME, accountName);
+ editor.commit();
+
+ fetchUserProfile();
+ }
+
+ private void fetchUserProfile() {
+ AccountManager manager = (AccountManager) getSystemService(Context.ACCOUNT_SERVICE);
+ Account[] accounts = manager.getAccountsByType("com.google");
+ Account account = null;
+ for (int i = 0; i < accounts.length; i++) {
+ if (accounts[i].name.equals(mAccountName)) {
+ account = accounts[i];
+ break;
+ }
+ }
+ if (account == null) {
+ Log.e(TAG, "Couldn't find Google account with name: " + mAccountName);
+ pickAccount();
+ return;
+ }
+ manager.getAuthToken(account,
+ OAUTH_SCOPE,
+ new Bundle(),
+ false,
+ new OnTokenAcquired(),
+ new Handler(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ Log.e(TAG, "Error getting auth token: " + msg.toString());
+ fetchUserProfileDone(null);
+ return true;
+ }
+ }));
+ }
+
+ private void fetchUserProfileDone(JSONObject userProfile) {
+ if (userProfile != null) {
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putString(PREF_USER_PROFILE_JSON, userProfile.toString());
+ editor.commit();
+ }
+
+ finishActivity();
+ }
+
+
+ private void finishActivity() {
+ mProgressDialog.dismiss();
+ startActivity(new Intent(this, DeckChooserActivity.class));
+ finish();
+ }
+
+ private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
+ @Override
+ public void run(AccountManagerFuture<Bundle> result) {
+ try {
+ Bundle bundle = result.getResult();
+ Intent launch = (Intent) bundle.get(AccountManager.KEY_INTENT);
+ if (launch != null) { // Needs user approval.
+ // NOTE(spetrovic): The returned intent has the wrong flag value
+ // FLAG_ACTIVITY_NEW_TASK set, which results in the launched intent replying
+ // immediately with RESULT_CANCELED. Hence, we clear the flag here.
+ launch.setFlags(0);
+ startActivityForResult(launch, REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL);
+ return;
+ }
+ String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
+ new ProfileInfoFetcher().execute(token);
+ } catch (AuthenticatorException e) {
+ Log.e(TAG, "Couldn't authorize: " + e.getMessage());
+ fetchUserProfileDone(null);
+ } catch (OperationCanceledException e) {
+ Log.e(TAG, "Authorization cancelled: " + e.getMessage());
+ fetchUserProfileDone(null);
+ } catch (IOException e) {
+ Log.e(TAG, "Unexpected error: " + e.getMessage());
+ fetchUserProfileDone(null);
+ }
+ }
+ }
+
+ private class ProfileInfoFetcher extends AsyncTask<String, Void, JSONObject> {
+ @Override
+ protected JSONObject doInBackground(String... params) {
+ try {
+ URL url = new URL(OAUTH_USERINFO_URL + "?access_token=" + params[0]);
+ return new JSONObject(CharStreams.toString(
+ new InputStreamReader(url.openConnection().getInputStream(),
+ Charsets.US_ASCII)));
+ } catch (MalformedURLException e) {
+ Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+ } catch (JSONException e) {
+ Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+ } catch (IOException e) {
+ Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(JSONObject userProfile) {
+ fetchUserProfileDone(userProfile);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_sign_in, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == R.id.action_settings) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListFragment.java
index 06299c9..ba4070d 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListFragment.java
@@ -9,11 +9,13 @@
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.model.Role;
public class SlideListFragment extends Fragment {
private static final String DECK_ID_KEY = "deck_id";
@@ -62,6 +64,8 @@
bundle = getArguments();
}
mDeckId = bundle.getString(DECK_ID_KEY);
+ Log.d(TAG, "onCreateView - Got deckId = " + mDeckId);
+
mRole = (Role) bundle.get(ROLE_KEY);
}
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 d3c193a..668b7e0 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
@@ -6,10 +6,10 @@
import android.app.Activity;
import android.content.Context;
-import android.content.Intent;
import java.util.List;
+import io.v.android.apps.syncslides.misc.Config;
import io.v.android.apps.syncslides.model.Deck;
import io.v.android.apps.syncslides.model.Listener;
import io.v.android.apps.syncslides.model.Question;
@@ -22,15 +22,15 @@
public interface DB {
class Singleton {
private static volatile DB instance;
+
public static DB get(Context context) {
DB result = instance;
if (instance == null) {
synchronized (Singleton.class) {
result = instance;
if (result == null) {
- // Switch between FakeDB and SyncbaseDB by commenting out one.
- instance = result = new FakeDB(context);
- // instance = result = new SyncbaseDB(context);
+ instance = result = Config.Syncbase.ENABLE ?
+ new SyncbaseDB(context) : new FakeDB(context);
}
}
}
@@ -39,16 +39,18 @@
}
/**
- * 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.
+ * 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.
+ * @param activity implements onActivityResult() to call into
+ * DB.onActivityResult.
*/
void init(Activity activity);
/**
- * Provides a list of elements via an API that fits well with RecyclerView.Adapter.
+ * Provides a list of elements via an API that fits well with
+ * RecyclerView.Adapter.
*/
interface DBList<E> {
@@ -63,12 +65,14 @@
E get(int i);
/**
- * Sets the listener for changes to the list. There can only be one listener.
+ * Sets the listener for changes to the list. There can only be one
+ * listener.
*/
void setListener(Listener listener);
/**
- * Indicates that the list is no longer needed and should stop notifying its listener.
+ * Indicates that the list is no longer needed and should stop notifying
+ * its listener.
*/
void discard();
}
@@ -99,7 +103,7 @@
/**
* Asynchronously fetch the slides for the given deck.
*
- * @param deckId the deck to fetch
+ * @param deckId the deck to fetch
* @param callback runs on the UI thread when the slide data is loaded
*/
void getSlides(String deckId, Callback<List<Slide>> callback);
@@ -122,29 +126,36 @@
void importDeck(Deck deck, Slide[] slides, Callback<Void> callback);
/**
+ * Synchronously gets a deck by Id. Returns null if not found.
+ *
+ * @param deckId id of the deck to get
+ */
+ Deck getDeck(String deckId);
+
+ /**
* Asynchronously deletes the deck and all of its slides.
*
- * @param deckId id of the deck to delete
+ * @param deckId id of the deck to delete
*/
void deleteDeck(String deckId);
/**
* Asynchronously deletes the deck and all of its slides.
*
- * @param deckId id of the deck to delete
- * @param callback runs on the UI thread when the deck has been deleted
+ * @param deckId id of the deck to delete
+ * @param callback runs on the UI thread when the deck has been deleted
*/
void deleteDeck(String deckId, Callback<Void> callback);
class CreatePresentationResult {
/**
- * A unique ID for the presentation. All methods that deal with live presentation
- * data (e.g. the current slide) use this ID.
+ * A unique ID for the presentation. All methods that deal with live
+ * presentation data (e.g. the current slide) use this ID.
*/
public String presentationId;
/**
- * This is the name of the syncgroup that was created for this presentation instance.
- * Audience members must join this syncgroup.
+ * This is the name of the syncgroup that was created for this
+ * presentation instance. Audience members must join this syncgroup.
*/
public String syncgroupName;
@@ -157,7 +168,7 @@
/**
* Creates a new presentation by creating a syncgroup.
*
- * @param deckId the deck to use in the presentation
+ * @param deckId the deck to use in the presentation
* @param callback called when the presentation is created
*/
void createPresentation(String deckId, Callback<CreatePresentationResult> callback);
@@ -166,16 +177,16 @@
* Joins an existing presentation.
*
* @param syncgroupName the syncgroup to join
- * @param callback called when the syncgroup is joined
+ * @param callback called when the syncgroup is joined
*/
void joinPresentation(String syncgroupName, Callback<Void> callback);
/**
* Sets the current slide so any audience members can switch to it.
*
- * @param deckId the deck being presented
+ * @param deckId the deck being presented
* @param presentationId the instance of the live presentation
- * @param slideNum the new slide number
+ * @param slideNum the new slide number
*/
void setCurrentSlide(String deckId, String presentationId, int slideNum);
@@ -191,9 +202,9 @@
/**
* Add a listener for changes to the current slide of a live presentation.
*
- * @param deckId the deck used in the presentation
+ * @param deckId the deck used in the presentation
* @param presentationId the presentation to watch for changes
- * @param listener notified of changes
+ * @param listener notified of changes
*/
void addCurrentSlideListener(String deckId, String presentationId,
CurrentSlideListener listener);
@@ -201,9 +212,9 @@
/**
* Remove a listener that was previously passed to addCurrentSlideListener().
*
- * @param deckId the deck used in the presentation
+ * @param deckId the deck used in the presentation
* @param presentationId the presentation being watched for changes
- * @param listener previously passed to addCurrentSlideListener()
+ * @param listener previously passed to addCurrentSlideListener()
*/
void removeCurrentSlideListener(String deckId, String presentationId,
CurrentSlideListener listener);
@@ -218,12 +229,12 @@
}
/**
- * Set the listener for changes to the set of questions for a live presentation.
- * There can be only one listener at a time.
+ * Set the listener for changes to the set of questions for a live
+ * presentation. There can be only one listener at a time.
*
- * @param deckId the deck used in the presentation
+ * @param deckId the deck used in the presentation
* @param presentationId the presentation to watch for changes
- * @param listener notified of changes
+ * @param listener notified of changes
*/
void setQuestionListener(String deckId, String presentationId,
QuestionListener listener);
@@ -231,9 +242,9 @@
/**
* Remove the listener that was previously passed to setQuestionListener().
*
- * @param deckId the deck used in the presentation
+ * @param deckId the deck used in the presentation
* @param presentationId the presentation being watched for changes
- * @param listener previously passed to setQuestionListener()
+ * @param listener previously passed to setQuestionListener()
*/
void removeQuestionListener(String deckId, String presentationId,
QuestionListener listener);
@@ -241,10 +252,10 @@
/**
* Add user to presenter's question queue.
*
- * @param deckId the deck used in the presentation
+ * @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 firstName the user's first name
+ * @param lastName the user's last name
*/
void askQuestion(String deckId, String presentationId,
String firstName, String lastName);
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 7eaddeb..cd41a7d 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
@@ -6,32 +6,28 @@
import android.app.Activity;
import android.content.Context;
-import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
-import java.util.Collections;
-import java.util.List;
-
-import com.google.common.collect.ImmutableList;
-
import org.joda.time.DateTime;
-import org.joda.time.Duration;
import org.joda.time.Period;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Random;
import io.v.android.apps.syncslides.R;
import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Listener;
import io.v.android.apps.syncslides.model.Question;
import io.v.android.apps.syncslides.model.Slide;
@@ -91,10 +87,8 @@
}
mHandler = new Handler(Looper.getMainLooper());
for (int i = 0; i < DECKTHUMBS.length; ++i) {
- mDecks.add(new DeckImpl(
- DECKTITLES[i],
- BitmapFactory.decodeResource(context.getResources(), DECKTHUMBS[i]),
- String.valueOf(i)));
+ mDecks.add(DeckFactory.Singleton.get(context).make(
+ DECKTITLES[i], DECKTHUMBS[i], i));
mSlides.put(String.valueOf(i), new FakeSlideList(slides));
}
mCurrentSlideListeners = Lists.newArrayList();
@@ -110,9 +104,9 @@
Random random = new Random();
for (int i = 0; i < 5; i++) {
Question question = new Question(
- "queston"+i,
+ "question" + i,
"Questioner",
- "#"+i,
+ "#" + i,
DateTime.now().minus(Period.minutes(random.nextInt(5))));
mQuestions.add(question);
}
@@ -332,6 +326,17 @@
}
@Override
+ public Deck getDeck(String deckId) {
+ for (int i = 0; i < mDecks.getItemCount(); i++) {
+ Deck result = mDecks.get(i);
+ if (result.getId().equals(deckId)) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
public void deleteDeck(String deckId) {
mDecks.delete(deckId);
}
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 e6b487a..a599bdb 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
@@ -31,9 +31,9 @@
import java.util.UUID;
import io.v.android.apps.syncslides.R;
-import io.v.android.apps.syncslides.discovery.V23Manager;
+import io.v.android.apps.syncslides.misc.V23Manager;
import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Listener;
import io.v.android.apps.syncslides.model.NoopList;
import io.v.android.apps.syncslides.model.Slide;
@@ -93,11 +93,13 @@
private final Map<String, CurrentSlideWatcher> mCurrentSlideWatchers;
private final Map<String, QuestionWatcher> mQuestionWatchers;
private Server mSyncbaseServer;
+ private final DeckFactory mDeckFactory;
SyncbaseDB(Context context) {
mContext = context;
mCurrentSlideWatchers = Maps.newHashMap();
mQuestionWatchers = Maps.newHashMap();
+ mDeckFactory = DeckFactory.Singleton.get(context);
}
@Override
@@ -122,7 +124,7 @@
// work so DB methods should return noop values. It's assumed that
// the calling fragment will send the user to the AccountManager,
// accept blessings on return, then re-call this init.
- if (V23Manager.Singleton.get().isBlessed()) {
+ if (!V23Manager.Singleton.get().isBlessed()) {
Log.d(TAG, "no blessings.");
return;
}
@@ -279,6 +281,7 @@
@Override
public void run() {
try {
+ Log.i(TAG, "Joining: " + syncgroupName);
Syncgroup syncgroup = mDB.getSyncgroup(syncgroupName);
syncgroup.join(mVContext, new SyncgroupMemberInfo((byte) 1));
for (String member : syncgroup.getMembers(mVContext).keySet()) {
@@ -304,22 +307,24 @@
if (!mInitialized) {
return new NoopList<>();
}
- return new DeckList(mVContext, mDB);
+ return new DeckList(mVContext, mDB, mDeckFactory);
}
- private static class DeckList implements DBList {
+ private static class DeckList implements DBList<Deck> {
private final CancelableVContext mVContext;
private final Database mDB;
+ private final DeckFactory mDeckFactory;
private final Handler mHandler;
private ResumeMarker mWatchMarker;
- private volatile boolean mIsDiscarded;
- private volatile Listener mListener;
+ private boolean mIsDiscarded;
+ private Listener mListener;
private List<Deck> mDecks;
- public DeckList(VContext vContext, Database db) {
+ public DeckList(VContext vContext, Database db, DeckFactory df) {
mVContext = vContext.withCancel();
mDB = db;
+ mDeckFactory = df;
mIsDiscarded = false;
mDecks = Lists.newArrayList();
mHandler = new Handler(Looper.getMainLooper());
@@ -345,12 +350,7 @@
}
String key = (String) row.get(0).getElem();
Log.i(TAG, "Fetched deck " + key);
- VDeck vDeck = (VDeck) row.get(1).getElem();
- final Deck deck = new DeckImpl(
- vDeck.getTitle(),
- BitmapFactory.decodeByteArray(
- vDeck.getThumbnail(), 0, vDeck.getThumbnail().length),
- key);
+ final Deck deck = mDeckFactory.make((VDeck) row.get(1).getElem(), key);
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -393,11 +393,7 @@
} catch (VException e) {
Log.e(TAG, "Couldn't decode deck: " + e.toString());
}
- final Deck deck =
- new DeckImpl(vDeck.getTitle(),
- BitmapFactory.decodeByteArray(
- vDeck.getThumbnail(), 0, vDeck.getThumbnail().length),
- key);
+ final Deck deck = mDeckFactory.make(vDeck, key);
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -439,18 +435,20 @@
}
@Override
- public synchronized void discard() {
+ public void discard() {
Log.i(TAG, "Discarding deck list.");
+ mIsDiscarded = true;
mVContext.cancel(); // this will cause the watcher thread to exit
mHandler.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. (Note that
- // put()/get() are synchronized along with this method.)
- mIsDiscarded = true;
}
- private synchronized void put(Deck deck) {
+ private void put(Deck deck) {
+ // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+ // been called), because that method doesn't prevent future post()s being made on
+ // the handler. So the following scenario is possible:
+ // - fetcher thread is about to execute post().
+ // - discard clears all pending messages from the handler.
+ // - fetcher executes the post().
if (mIsDiscarded) {
return;
}
@@ -477,7 +475,13 @@
}
}
- private synchronized void delete(String deckId) {
+ private void delete(String deckId) {
+ // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+ // been called), because that method doesn't prevent future post()s being made on
+ // the handler. So the following scenario is possible:
+ // - fetcher thread is about to execute post().
+ // - discard clears all pending messages from the handler.
+ // - fetcher executes the post().
if (mIsDiscarded) {
return;
}
@@ -552,7 +556,7 @@
private final Handler mHandler;
private final String mDeckId;
private ResumeMarker mWatchMarker;
- private volatile boolean mIsDiscarded;
+ private boolean mIsDiscarded;
// Storage for slides, mirroring the slides in the Syncbase. Since slide numbers can
// have "holes" in them (e.g., 1, 2, 4, 6, 8), we maintain a map from slide key
// to the slide, as well as an ordered list which is returned to the caller.
@@ -684,18 +688,20 @@
}
@Override
- public synchronized void discard() {
+ public void discard() {
Log.i(TAG, "Discarding slides list");
+ mIsDiscarded = true;
mVContext.cancel(); // this will cause the watcher thread to exit
mHandler.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. (Note that
- // put()/get() are synchronized along with this method.)
- mIsDiscarded = true;
}
- private synchronized void put(String key, Slide slide) {
+ private void put(String key, Slide slide) {
+ // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+ // been called), because that method doesn't prevent future post()s being made on
+ // the handler. So the following scenario is possible:
+ // - fetcher thread is about to execute post().
+ // - discard clears all pending messages from the handler.
+ // - fetcher executes the post().
if (mIsDiscarded) {
return;
}
@@ -715,7 +721,13 @@
}
}
- private synchronized void delete(String key) {
+ private void delete(String key) {
+ // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+ // been called), because that method doesn't prevent future post()s being made on
+ // the handler. So the following scenario is possible:
+ // - fetcher thread is about to execute post().
+ // - discard clears all pending messages from the handler.
+ // - fetcher executes the post().
if (mIsDiscarded) {
return;
}
@@ -949,6 +961,20 @@
}
@Override
+ public Deck getDeck(String deckId) {
+ VDeck vDeck = null;
+ try {
+ vDeck = (VDeck) mDecks.get(mVContext, deckId, VDeck.class);
+ } catch (VException e) {
+ handleError(e.toString());
+ }
+ if (vDeck != null) {
+ return mDeckFactory.make(vDeck, deckId);
+ }
+ return null;
+ }
+
+ @Override
public void deleteDeck(String deckId) {
try {
mDecks.deleteRange(mVContext, RowRange.prefix(deckId));
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 539d5b8..57321c8 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
@@ -13,6 +13,8 @@
import java.util.List;
import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.misc.Config;
+import io.v.android.apps.syncslides.misc.V23Manager;
import io.v.android.apps.syncslides.model.Deck;
import io.v.android.apps.syncslides.model.Listener;
import io.v.android.apps.syncslides.model.Participant;
@@ -37,19 +39,19 @@
private final Handler mHandler;
private Listener mListener;
- public static DiscoveryManager make() {
+ public static DiscoveryManager make(Context context) {
// If blessings not in place, use fake data.
boolean useRealDiscovery =
- Participant.ENABLE_MT_DISCOVERY &&
+ Config.MtDiscovery.ENABLE &&
V23Manager.Singleton.get().isBlessed();
if (useRealDiscovery) {
Log.d(TAG, "Using real discovery.");
return new DiscoveryManager(
- new Moderator(new ParticipantScannerMt()));
+ new Moderator(new ParticipantScannerMt(context)));
}
Log.d(TAG, "Using fake discovery.");
return new DiscoveryManager(
- new Moderator(new ParticipantScannerFake()));
+ new Moderator(new ParticipantScannerFake(context)));
}
private DiscoveryManager(Moderator moderator) {
@@ -121,7 +123,6 @@
public void discard() {
Log.d(TAG, "Discarding.");
stop();
- mListener = null;
}
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/Moderator.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/Moderator.java
index 4e2f6c8..6354634 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/Moderator.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/Moderator.java
@@ -37,8 +37,6 @@
private final ParticipantScanner mScanner;
// Notify this guy when task done; make it a list if more needed.
private Observer mObserver;
- // Counts runs for debugging.
- private int mCounter = 0;
public Moderator(ParticipantScanner scanner) {
mScanner = scanner;
@@ -62,7 +60,6 @@
throw new IllegalStateException("Must have an observer.");
}
try {
- mCounter++;
process(mScanner.scan());
mObserver.onTaskDone();
} catch (Throwable t) {
@@ -78,11 +75,13 @@
mSenior.clear();
mFreshman.clear();
+ Set<Participant> potentials = new HashSet<>();
+
for (Participant p : latest) {
if (current.contains(p)) {
mSenior.add(p);
} else {
- mFreshman.add(p);
+ potentials.add(p);
}
}
@@ -93,8 +92,11 @@
}
}
- for (Participant p : mFreshman) {
- p.refreshData();
+
+ for (Participant p : potentials) {
+ if (p.refreshData()) {
+ mFreshman.add(p);
+ }
}
}
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 9452f5b..ff7e70c 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
@@ -4,15 +4,22 @@
package io.v.android.apps.syncslides.discovery;
+import android.graphics.Bitmap;
import android.util.Log;
import org.joda.time.DateTime;
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.misc.V23Manager;
import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Participant;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.ServerCall;
import io.v.v23.verror.VException;
/**
@@ -35,7 +42,7 @@
private static final DateTimeFormatter TIME_FMT =
DateTimeFormat.forPattern("hh_mm_ss_SSSS");
// V23 name of the V23 service representing the participant.
- private String mServiceName;
+ private final String mServiceName;
// Visible name of human presenter.
// TODO(jregan): Switch to VPerson or the model equivalent.
private String mUserName;
@@ -43,25 +50,30 @@
private DateTime mRefreshTime;
// Deck the user is presenting. Can only present one at a time.
private Deck mDeck;
- private ParticipantClient mClient = null;
+ // Used to make decks after RPCs.
+ private final DeckFactory mDeckFactory;
- public ParticipantPeer(String userName, Deck deck, String serviceName) {
+ private ParticipantPeer(
+ String userName, Deck deck, String serviceName, DeckFactory deckFactory) {
mUserName = userName;
mDeck = deck;
mServiceName = serviceName;
+ mDeckFactory = deckFactory;
}
- public ParticipantPeer(String userName, Deck deck) {
- this(userName, deck, Unknown.SERVER_NAME);
+ public static ParticipantPeer makeWithServiceName(
+ String serviceName, DeckFactory deckFactory) {
+ return new ParticipantPeer(Unknown.USER_NAME, null, serviceName, deckFactory);
}
- public ParticipantPeer(String serviceName) {
- this(Unknown.USER_NAME, DeckImpl.DUMMY, serviceName);
+ public static ParticipantPeer makeWithKnownDeck(String userName, Deck deck) {
+ return new ParticipantPeer(userName, deck, Unknown.SERVER_NAME, null);
}
@Override
public String getServiceName() {
- return mServiceName;
+ return (mServiceName != null && !mServiceName.isEmpty()) ?
+ mServiceName : Unknown.SERVER_NAME;
}
@Override
@@ -92,41 +104,87 @@
return false;
}
ParticipantPeer p = (ParticipantPeer) obj;
- return mServiceName.equals(p.mServiceName) && mDeck.equals(p.mDeck);
+ boolean deckEqual = (mDeck == null) ? true : mDeck.equals(p.mDeck);
+ return deckEqual && getServiceName().equals(p.getServiceName());
}
@Override
public int hashCode() {
- return mServiceName.hashCode() + mDeck.hashCode();
+ int deckCode = (mDeck == null) ? 0 : mDeck.hashCode();
+ return deckCode + getServiceName().hashCode();
}
/**
* Make an RPC on the mServiceName to get title, snapshot, etc.
*/
@Override
- public void refreshData() {
- Log.d(TAG, "Initiating refresh");
-
- if (mClient == null) {
- Log.d(TAG, "Grabbing client.");
- mClient = ParticipantClientFactory.getParticipantClient(
- mServiceName);
- Log.d(TAG, "Got client.");
+ public boolean refreshData() {
+ if (mServiceName.equals(Unknown.SERVER_NAME)) {
+ // Don't attempt refresh.
+ return true;
}
+ Log.d(TAG, "refreshData");
+ // Flush, since the server might have died and restarted, invalidating
+ // cached endpoints.
+ Log.d(TAG, "Flushing cache for service " + mServiceName);
+ V23Manager.Singleton.get().flushServerFromCache(mServiceName);
+ ParticipantClient client =
+ ParticipantClientFactory.getParticipantClient(mServiceName);
+ Log.d(TAG, "Got client = " + client.toString());
try {
- Log.d(TAG, "Calling get");
- Description description = mClient.get(
+ Log.d(TAG, "Calling get...");
+ VDeck vDeck = client.get(
V23Manager.Singleton.get().getVContext());
- mDeck = new DeckImpl(description.getTitle());
+ 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");
mRefreshTime = DateTime.now();
- Log.d(TAG, "Completed refresh.");
+ mDeck = newDeck;
+ Log.d(TAG, " Got deck = " + mDeck);
+ return true;
} catch (VException e) {
+ Log.d(TAG, "RPC failed, leaving current deck in place.");
e.printStackTrace();
}
+ return false;
}
private static class Unknown {
static final String SERVER_NAME = "unknownServerName";
static final String USER_NAME = "unknownUserName";
}
+
+ /**
+ * Serves data used in deck discovery.
+ */
+ public static class Server implements ParticipantServer {
+ private static final String TAG = "ParticipantServer";
+ private final Deck mDeck;
+
+ public Server(Deck d) {
+ mDeck = d;
+ }
+
+ public VDeck 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;
+ }
+ }
}
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 0d6defd..7db977e 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
@@ -4,32 +4,44 @@
package io.v.android.apps.syncslides.discovery;
+import android.content.Context;
+
import java.util.HashSet;
import java.util.Set;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Participant;
public class ParticipantScannerFake implements ParticipantScanner {
private static final String TAG = "ParticipantScannerFake";
+ protected final DeckFactory mDeckFactory;
+
private int mCounter = 0;
+ public ParticipantScannerFake(Context context) {
+ this.mDeckFactory = DeckFactory.Singleton.get(context);
+ }
+
public Set<Participant> scan() {
mCounter = (mCounter + 1) % 10;
HashSet<Participant> participants = new HashSet<>();
if (mCounter >= 2 && mCounter <= 8) {
participants.add(
- new ParticipantPeer(
- "Alice", new DeckImpl(
- "Kale - Just eat it.", null, "deckByAlice")));
+ ParticipantPeer.makeWithKnownDeck(
+ "Alice",
+ mDeckFactory.make(
+ "Kale - Just eat it.",
+ "deckByAlice")));
}
// Bob has less to say than Alice.
if (mCounter >= 4 && mCounter <= 6) {
participants.add(
- new ParticipantPeer(
- "Bob", new DeckImpl(
- "Java - Object deluge.", null, "deckByBob")));
+ ParticipantPeer.makeWithKnownDeck(
+ "Bob",
+ mDeckFactory.make(
+ "Java - Object deluge.",
+ "deckByBob")));
}
return participants;
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerMt.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerMt.java
index af17b3c..4ba8c32 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerMt.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerMt.java
@@ -4,11 +4,15 @@
package io.v.android.apps.syncslides.discovery;
+import android.content.Context;
import android.util.Log;
import java.util.HashSet;
import java.util.Set;
+import io.v.android.apps.syncslides.misc.Config;
+import io.v.android.apps.syncslides.misc.V23Manager;
+import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Participant;
/**
@@ -17,13 +21,19 @@
public class ParticipantScannerMt implements ParticipantScanner {
private static final String TAG = "ParticipantScannerMt";
+ protected final DeckFactory mDeckFactory;
+
+ public ParticipantScannerMt(Context context) {
+ this.mDeckFactory = DeckFactory.Singleton.get(context);
+ }
+
@Override
public Set<Participant> scan() {
Set<Participant> result = new HashSet<>();
for (String n : V23Manager.Singleton.get().scan(
- Participant.Mt.makeScanString())) {
+ Config.MtDiscovery.makeScanString())) {
Log.d(TAG, "Found: " + n);
- result.add(new ParticipantPeer(n));
+ result.add(ParticipantPeer.makeWithServiceName(n, mDeckFactory));
}
return result;
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantServerImpl.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantServerImpl.java
deleted file mode 100644
index fbd251a..0000000
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantServerImpl.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// 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.discovery;
-
-import android.util.Log;
-
-import io.v.android.apps.syncslides.model.Deck;
-import io.v.v23.context.VContext;
-import io.v.v23.rpc.ServerCall;
-
-/**
- * Serves data used in deck discovery.
- */
-public class ParticipantServerImpl implements ParticipantServer {
- private static final String TAG = "PresentationActivity";
- private final Deck mDeck;
-
- public ParticipantServerImpl(Deck d) {
- mDeck = d;
- }
-
- public Description get(VContext ctx, ServerCall call)
- throws io.v.v23.verror.VException {
- Log.d(TAG, "Responding to Get RPC.");
- Description d = new Description();
- d.setTitle(mDeck.getTitle());
- d.setUserName(mDeck.getId());
- return d;
- }
-}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/PeriodicTasker.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/PeriodicTasker.java
index 306c63f..bdd2b21 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/PeriodicTasker.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/PeriodicTasker.java
@@ -8,13 +8,12 @@
import org.joda.time.Duration;
-import java.util.Timer;
-import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import io.v.android.apps.syncslides.misc.V23Manager;
+
/**
* Repeatedly runs a task in a thread distinct from that which calls start().
*/
@@ -22,10 +21,10 @@
private static final String TAG = "PeriodicTasker";
private static final Duration DELAY_BEFORE_FIRST_TASK =
- Duration.standardSeconds(2);
+ Duration.standardSeconds(1);
private static final Duration WAIT_BETWEEN_TASKS =
- V23Manager.MT_TIMEOUT.plus(Duration.standardSeconds(3));
+ V23Manager.MT_TIMEOUT.plus(Duration.millis(500));
private ScheduledExecutorService mTimer = null;
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 7965344..6b011c9 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,20 +8,6 @@
"io/v/android/apps/syncslides/db"
)
-type Description struct {
- UserName string
- Title string
-}
-
type Participant interface {
- Get() (Description | error)
-}
-
-// TODO(jregan): Ditch Description in favor of db.VDeck.
-// so we get thumb automatically. Rewire the remaining
-// code. Add whatever else we need.
-// This is just a test of the code generator, and should
-// be removed shortly.
-type ChuckleParticipant interface {
Get() (db.VDeck | 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
new file mode 100644
index 0000000..ee26d1f
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java
@@ -0,0 +1,61 @@
+// 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.misc;
+
+import io.v.android.apps.syncslides.model.Deck;
+
+/**
+ * Syncslides configuration.
+ */
+public class Config {
+ /**
+ * Which Mounttable to use for everything.
+ */
+ public static final String MT_ADDRESS = Tables.PI_MILK_CRATE;
+
+ /**
+ * Some fixed mount tables to try.
+ */
+ private static class Tables {
+ static final String PI_MILK_CRATE = "192.168.86.254:8101";
+ static final String JR_LAPTOP_AT_HOME = "192.168.2.71:23000";
+ static final String JR_LAPTOP_VEYRON = "192.168.8.106:23000";
+ static final String JR_MOTOX = "192.168.43.136:23000";
+ }
+
+ public static class Syncbase {
+ /**
+ * If true, enable use of syncbase as the DB, else use a fake.
+ */
+ public static final boolean ENABLE = false;
+ }
+
+ public static class MtDiscovery {
+ /**
+ * If true, enable MT-based (mounttable) discovery, else use a fake. If
+ * enabled, DeckChooserActivity will scan a MT to find live
+ * presentations. Clicking play on a presentation will start a service
+ * and try to mount it in a MT so other deck views can list it in the
+ * UX.
+ */
+ public static final boolean ENABLE = true;
+ /**
+ * Every v23 service will be mounted in the namespace with a name
+ * prefixed by this.
+ */
+ private static String LIVE_PRESENTATION_PREFIX = "liveDeck";
+
+ /**
+ * TODO(jregan): Assure legal mount name (remove blanks and such).
+ */
+ public static String makeMountName(Deck deck) {
+ return LIVE_PRESENTATION_PREFIX + "/" + deck.getId();
+ }
+
+ public static String makeScanString() {
+ return LIVE_PRESENTATION_PREFIX + "/*";
+ }
+ }
+}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/V23Manager.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
similarity index 94%
rename from projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/V23Manager.java
rename to projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
index 1966ab3..734d79e 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/V23Manager.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
@@ -2,11 +2,13 @@
// 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.discovery;
+package io.v.android.apps.syncslides.misc;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
@@ -20,6 +22,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import io.v.android.apps.syncslides.SignInActivity;
import io.v.android.libs.security.BlessingsManager;
import io.v.android.v23.V;
import io.v.android.v23.services.blessing.BlessingCreationException;
@@ -56,8 +59,6 @@
private static final String TAG = "V23Manager";
private static final ExecutorService mExecutor =
Executors.newSingleThreadExecutor();
- private static final String MT_ADDRESS = FixedMt.PI_MILK_CRATE;
- // private static final String MT_ADDRESS = FixedMt.JR_MOTOX;
private Context mAndroidCtx;
private VContext mBaseContext = null;
private Blessings mBlessings = null;
@@ -124,12 +125,12 @@
*/
public static List<String> determineNamespaceRoot() {
List<String> result = new ArrayList<>();
- result.add("/" + MT_ADDRESS);
+ result.add("/" + Config.MT_ADDRESS);
return result;
}
public static String syncName(String id) {
- return NamingUtil.join("/", MT_ADDRESS, id);
+ return NamingUtil.join("/", Config.MT_ADDRESS, id);
}
public VContext getVContext() {
@@ -174,14 +175,21 @@
throw new IllegalArgumentException(
"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, "");
activity.startActivityForResult(
- BlessingService.newBlessingIntent(androidCtx),
+ BlessingService.newBlessingIntent(androidCtx, userEmail),
BLESSING_REQUEST);
return;
}
asyncConfigurePrincipal(blessings);
}
+ public void flushServerFromCache(String name) {
+ V.getNamespace(mBaseContext).flushCacheEntry(mBaseContext, name);
+ }
+
/**
* v23 operations that require a blessing (almost everything) will fail if
* attempted before this is true.
@@ -311,7 +319,7 @@
@Override
public void run() {
Log.d(TAG, "mounting on name \"" + mountName +
- "\" at table " + MT_ADDRESS);
+ "\" at table " + Config.MT_ADDRESS);
try {
mLiveServer = makeServer(mountName, server);
Log.d(TAG, " Server status proxies: " +
@@ -379,13 +387,4 @@
}
}
- /**
- * Some fixed mount tables to try.
- */
- private static class FixedMt {
- static final String PI_MILK_CRATE = "192.168.86.254:8101";
- static final String JR_LAPTOP_AT_HOME = "192.168.2.71:23000";
- static final String JR_LAPTOP_VEYRON = "192.168.8.106:23000";
- static final String JR_MOTOX = "192.168.43.136:23000";
- }
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Deck.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Deck.java
index adf2ec0..642894a 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Deck.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Deck.java
@@ -28,17 +28,9 @@
String getId();
/**
- * Returns a bundled form of the instance; pass null for a new bundle,
- * pass an existing bundle to overwrite its fields.
- */
- Bundle toBundle(Bundle b);
-
- /**
- * Keys for Bundle fields.
+ * Keys for Bundle/Intent fields.
*/
class B {
public static final String DECK_ID = "deck_id";
- public static final String DECK_TITLE = "deck_title";
- public static final String DECK_THUMB = "deck_thumb";
}
}
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
new file mode 100644
index 0000000..1dec64c
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java
@@ -0,0 +1,96 @@
+// 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.model;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import io.v.android.apps.syncslides.R;
+import io.v.android.apps.syncslides.db.VDeck;
+
+/**
+ * One place to make consistent decks with known defaults.
+ */
+public class DeckFactory {
+ private static final String TAG = "DeckFactory";
+ protected final Bitmap mDefaultThumb;
+ private final Context mContext;
+
+ // Singleton
+ private DeckFactory(Context c) {
+ mContext = c;
+ mDefaultThumb = makeDefaultThumb(c);
+ }
+
+ private static Bitmap makeDefaultThumb(Context c) {
+ return BitmapFactory.decodeResource(
+ c.getResources(), R.drawable.thumb_deck1);
+ }
+
+ public Deck make() {
+ return make(Unknown.TITLE, Unknown.ID);
+ }
+
+ public Deck make(String title, String id) {
+ return make(title, mDefaultThumb, id);
+ }
+
+ public Deck make(String title, int index, int id) {
+ return make(
+ title,
+ BitmapFactory.decodeResource(mContext.getResources(), index),
+ String.valueOf(id));
+ }
+
+ public Deck make(VDeck vDeck, String id) {
+ if (vDeck.getThumbnail() == null) {
+ Log.d(TAG, "vDeck missing thumb; vdeck = " + vDeck);
+ }
+ return make(
+ vDeck.getTitle(),
+ vDeck.getThumbnail() == null ? null :
+ BitmapFactory.decodeByteArray(
+ vDeck.getThumbnail(), 0, vDeck.getThumbnail().length),
+ id);
+ }
+
+ public Deck make(String title, Bitmap thumb, String id) {
+ title = (title == null || title.isEmpty()) ? Unknown.TITLE : title;
+ thumb = (thumb == null) ? mDefaultThumb : thumb;
+ id = (id == null || id.isEmpty()) ? Unknown.ID : id;
+ return new DeckImpl(title, thumb, id);
+ }
+
+ public static class Singleton {
+ private static volatile DeckFactory instance;
+
+ public static DeckFactory get(Context c) {
+ DeckFactory result = instance;
+ if (instance == null) {
+ synchronized (Singleton.class) {
+ result = instance;
+ if (result == null) {
+ instance = result = new DeckFactory(c);
+ }
+ }
+ }
+ return result;
+ }
+
+ public static DeckFactory get() {
+ if (instance == null) {
+ throw new IllegalStateException("Must initialize with context.");
+ }
+ return instance;
+ }
+ }
+
+ private static class Unknown {
+ static final String TITLE = "unknownTitle";
+ static final String ID = "unknownId";
+ }
+}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java
index b90e061..d5993b4 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java
@@ -11,9 +11,6 @@
* Application impl of Deck.
*/
public class DeckImpl implements Deck {
- // For demos, debugging.
- public static final Deck DUMMY = new DeckImpl(
- Unknown.TITLE, Unknown.THUMB, Unknown.ID);
private final String mTitle;
private final Bitmap mThumb;
@@ -25,18 +22,10 @@
mDeckId = deckId;
}
- public DeckImpl(String title, Bitmap thumb) {
- this(title, thumb, Unknown.ID);
- }
-
- public DeckImpl(String title) {
- this(title, Unknown.THUMB);
- }
-
public String toString() {
- return "[title=\""+ (mTitle == null ? "unknown" : mTitle) +
- "\", id=" + (mDeckId == null ? "unknown" : mDeckId) +
- ", thumb=" + (mThumb == null ? "no" : "yes") + "]";
+ return "[title=\"" + (mTitle == null ? "unknown" : mTitle) +
+ "\", id=" + (mDeckId == null ? "unknown" : mDeckId) +
+ ", thumb=" + (mThumb == null ? "no" : "yes") + "]";
}
@Override
@@ -53,30 +42,6 @@
return mDeckId.hashCode();
}
- public static Deck fromBundle(Bundle b) {
- if (b == null) {
- throw new IllegalArgumentException("Need a bundle.");
- }
- return new DeckImpl(
- b.getString(B.DECK_TITLE),
- (Bitmap) b.getParcelable(B.DECK_THUMB),
- b.getString(B.DECK_ID));
- }
-
- @Override
- public Bundle toBundle(Bundle b) {
- if (b == null) {
- b = new Bundle();
- }
- b.putString(B.DECK_TITLE, mTitle);
- // TODO(jregan): Our thumbnails are too big! We need to store
- // them on disk, pass a file handle in the intent instead,
- // and load them on the other side.
- // ### b.putParcelable(B.DECK_THUMB, mThumb);
- b.putString(B.DECK_ID, mDeckId);
- return b;
- }
-
@Override
public Bitmap getThumb() {
return mThumb;
@@ -92,9 +57,4 @@
return mDeckId;
}
- private static class Unknown {
- static final String TITLE = "unknownTitle";
- static final String ID = "unknownId";
- static final Bitmap THUMB = null;
- }
}
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 7266789..fe83e5c 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,8 +4,6 @@
package io.v.android.apps.syncslides.model;
-import android.os.Bundle;
-
/**
* Someone taking part in a presentation.
*
@@ -13,33 +11,6 @@
* syncslides.
*/
public interface Participant {
- /**
- * If true, enable MT-based (mounttable) discovery. Deck view will scan a MT
- * to find live presentations. Clicking play on a presentation will start a
- * service and try to mount it in a MT so other deck views can list it in
- * the UX. MT location determined in
- * {@link io.v.android.apps.syncslides.discovery.V23Manager}.
- */
- boolean ENABLE_MT_DISCOVERY = true;
-
- public static class Mt {
- /**
- * Every v23 service will be mounted in the namespace with a name
- * prefixed by this.
- */
- public static String ROOT_NAME = "liveDeck";
-
- /**
- * TODO(jregan): Assure legal mount name (remove blanks and such).
- */
- public static String makeMountName(Deck deck) {
- return ROOT_NAME + "/" + deck.getId();
- }
-
- public static String makeScanString() {
- return ROOT_NAME + "/*";
- }
- }
// 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
@@ -53,7 +24,8 @@
String getServiceName();
// Initially get or refresh data from the endPoint.
- void refreshData();
+ // Return true if call succeeds, false otherwise.
+ boolean refreshData();
// For debugging.
String toString();
@@ -70,9 +42,6 @@
class B {
public static final String PARTICIPANT_ROLE = "participant_role";
public static final String PARTICIPANT_SHOULD_ADV = "participant_is_advertising";
- public static final String PARTICIPANT_SERVICE_NAME = "participant_endPoint";
- public static final String PARTICIPANT_NAME = "participant_name";
- public static final String PARTICIPANT_BLESSINGS = "participant_blessings";
public static final String PARTICIPANT_SYNCED = "participant_synced";
}
}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/Role.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Role.java
similarity index 92%
rename from projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/Role.java
rename to projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Role.java
index 7351e46..790d50c 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/Role.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Role.java
@@ -2,7 +2,7 @@
// 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;
+package io.v.android.apps.syncslides.model;
/**
* Represents the user's type of participation in the presentation.
diff --git a/projects/syncslides/app/src/main/res/layout-land/fragment_navigate.xml b/projects/syncslides/app/src/main/res/layout-land/fragment_navigate.xml
index e751d5f..41119a1 100644
--- a/projects/syncslides/app/src/main/res/layout-land/fragment_navigate.xml
+++ b/projects/syncslides/app/src/main/res/layout-land/fragment_navigate.xml
@@ -18,10 +18,10 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
+ android:background="@color/blue_grey_50"
android:gravity="left"
android:hint="@string/notes_hint"
- android:textSize="18sp"
- android:background="@color/blue_grey_50"/>
+ android:textSize="18sp" />
<RelativeLayout
android:layout_width="wrap_content"
@@ -33,6 +33,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:adjustViewBounds="true"
+ android:background="@color/blue_grey_50"
android:scaleType="fitCenter" />
<TextView
@@ -60,6 +61,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:adjustViewBounds="true"
+ android:background="@color/blue_grey_50"
android:scaleType="fitCenter" />
<TextView
diff --git a/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml b/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml
new file mode 100644
index 0000000..7c93107
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml
@@ -0,0 +1,9 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
+ android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ tools:context="io.v.android.apps.syncslides.SignInActivity">
+
+</RelativeLayout>
diff --git a/projects/syncslides/app/src/main/res/layout/fragment_navigate.xml b/projects/syncslides/app/src/main/res/layout/fragment_navigate.xml
index b821dfc..a4d2556 100644
--- a/projects/syncslides/app/src/main/res/layout/fragment_navigate.xml
+++ b/projects/syncslides/app/src/main/res/layout/fragment_navigate.xml
@@ -27,7 +27,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
- android:scaleType="fitCenter"/>
+ android:scaleType="fitCenter" />
<TextView
android:id="@+id/slide_num_text"
@@ -39,7 +39,7 @@
android:background="@drawable/nav_hint"
android:paddingLeft="@dimen/nav_hint_padding"
android:paddingRight="@dimen/nav_hint_padding"
- android:textColor="@color/nav_hint_text"/>
+ android:textColor="@color/nav_hint_text" />
</RelativeLayout>
@@ -86,10 +86,10 @@
android:layout_height="wrap_content"
android:layout_alignRight="@id/questions"
android:layout_alignTop="@id/questions"
- android:gravity="center"
- android:textSize="@dimen/nav_question_num_size"
android:background="@drawable/orange_circle"
- android:textColor="@color/nav_question_num_text"/>
+ android:gravity="center"
+ android:textColor="@color/nav_question_num_text"
+ android:textSize="@dimen/nav_question_num_size" />
</RelativeLayout>
@@ -140,12 +140,12 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
+ android:background="@color/blue_grey_50"
android:gravity="left"
android:hint="@string/notes_hint"
- android:textSize="18sp"
- android:scrollbars="vertical"
android:inputType="textMultiLine|textAutoComplete|textCapSentences"
- android:background="@color/blue_grey_50"/>
+ android:scrollbars="vertical"
+ android:textSize="18sp" />
<!-- Display the next and previous slide thumbnails. -->
<LinearLayout
@@ -166,6 +166,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:adjustViewBounds="true"
+ android:background="@color/blue_grey_50"
android:scaleType="fitCenter" />
<TextView
@@ -193,6 +194,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:adjustViewBounds="true"
+ android:background="@color/blue_grey_50"
android:scaleType="fitCenter" />
<TextView
diff --git a/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml b/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml
new file mode 100644
index 0000000..813fc46
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml
@@ -0,0 +1,7 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="io.v.android.apps.syncslides.SignInActivity">
+ <item android:id="@+id/action_settings" android:title="@string/action_settings"
+ android:orderInCategory="100" app:showAsAction="never" />
+</menu>
diff --git a/projects/syncslides/app/src/main/res/values/strings.xml b/projects/syncslides/app/src/main/res/values/strings.xml
index 7ffb985..48013cd 100644
--- a/projects/syncslides/app/src/main/res/values/strings.xml
+++ b/projects/syncslides/app/src/main/res/values/strings.xml
@@ -22,5 +22,4 @@
<string name="handoff_message">You handed off to</string>
<string name="end_handoff">RESUME</string>
<string name="presentation_live">LIVE NOW</string>
-
</resources>
diff --git a/settings.gradle b/settings.gradle
index 5b161dd..f626ccc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include 'lib', 'tests', 'android-lib', 'benchmarks:syncbench', 'projects:syncslides', 'projects:syncslides:app', 'projects:discovery_sample', 'projects:discovery_sample:app'
+include 'lib', 'tests', 'android-lib', 'benchmarks:syncbench', 'projects:syncslides', 'projects:syncslides:app', 'projects:syncslidepresenter', 'projects:discovery_sample', 'projects:discovery_sample:app'