Merge "syncslides: Fullscreen slide view."
diff --git a/android/app/src/main/java/io/v/syncslides/DeckChooserFragment.java b/android/app/src/main/java/io/v/syncslides/DeckChooserFragment.java
index e3284f9..a3b3cca 100644
--- a/android/app/src/main/java/io/v/syncslides/DeckChooserFragment.java
+++ b/android/app/src/main/java/io/v/syncslides/DeckChooserFragment.java
@@ -38,6 +38,7 @@
import java.util.UUID;
import io.v.syncslides.db.DB;
+import io.v.syncslides.discovery.RpcPresentationDiscovery;
import io.v.syncslides.lib.DeckImporter;
import io.v.syncslides.model.Deck;
import io.v.syncslides.model.DeckImpl;
@@ -98,7 +99,9 @@
mLayoutManager.requestLayout();
}
});
- mAdapter = new DeckListAdapter(DB.Singleton.get());
+ mAdapter = new DeckListAdapter(
+ DB.Singleton.get(),
+ new RpcPresentationDiscovery(V23.Singleton.get().getVContext()));
return rootView;
}
diff --git a/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java b/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java
index 81e59e1..58adc47 100644
--- a/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java
+++ b/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java
@@ -21,9 +21,11 @@
import java.util.Locale;
import io.v.syncslides.db.DB;
+import io.v.syncslides.discovery.PresentationDiscovery;
import io.v.syncslides.model.Deck;
import io.v.syncslides.model.DynamicList;
import io.v.syncslides.model.ListListener;
+import io.v.syncslides.model.PresentationAdvertisement;
import io.v.v23.verror.VException;
/**
@@ -35,25 +37,35 @@
private static final String TAG = "DeckListAdapter";
private final DB mDB;
+ private final PresentationDiscovery mDiscovery;
+ private DynamicList<PresentationAdvertisement> mLiveDecks;
private DynamicList<Deck> mDecks;
+ private OffsetListener mOffsetListener;
- public DeckListAdapter(DB db) {
+ public DeckListAdapter(DB db, PresentationDiscovery discovery) {
mDB = db;
+ mDiscovery = discovery;
}
/**
* Starts background monitoring of the underlying data.
*/
public void start() {
+ mLiveDecks = mDiscovery.scan();
+ mLiveDecks.addListener(this);
mDecks = mDB.getDecks();
- mDecks.addListener(this);
+ mOffsetListener = new OffsetListener();
+ mDecks.addListener(mOffsetListener);
}
/**
* Stops any background monitoring of the underlying data.
*/
public void stop() {
- mDecks.removeListener(this);
+ mLiveDecks.removeListener(this);
+ mLiveDecks = null;
+ mDecks.removeListener(mOffsetListener);
+ mOffsetListener = null;
mDecks = null;
}
@@ -66,44 +78,55 @@
@Override
public void onBindViewHolder(final ViewHolder holder, final int deckIndex) {
- final Deck deck = mDecks.get(deckIndex);
+ final Deck deck;
+ // 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 (deckIndex < mLiveDecks.getItemCount()) {
+ deck = mLiveDecks.get(deckIndex).getDeck();
+ holder.mToolbarLiveNow.setVisibility(View.VISIBLE);
+ holder.mToolbarLastOpened.setVisibility(View.GONE);
+ holder.mToolbar.getMenu().clear();
+ // TODO(kash): Click handler to join the presentation's syncgroup and start the
+ // PresentationActivity.
+ } else {
+ deck = mDecks.get(deckIndex - mLiveDecks.getItemCount());
- // TODO(afergan): Set actual date here.
- final Calendar cal = Calendar.getInstance();
- holder.mToolbarLastOpened.setText("Opened on "
- + cal.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.US) + " "
- + cal.get(Calendar.DAY_OF_MONTH) + ", " + cal.get(Calendar.YEAR));
+ // TODO(afergan): Set actual date here.
+ final Calendar cal = Calendar.getInstance();
+ holder.mToolbarLastOpened.setText("Opened on "
+ + cal.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.US) + " "
+ + cal.get(Calendar.DAY_OF_MONTH) + ", " + cal.get(Calendar.YEAR));
- holder.mToolbarLastOpened.setVisibility(View.VISIBLE);
- holder.mToolbarLiveNow.setVisibility(View.GONE);
- holder.mToolbar.setOnMenuItemClickListener(item -> {
- switch (item.getItemId()) {
- case R.id.action_delete_deck:
- // TODO(kash): Implement delete.
- // mDB.deleteDeck(deck.getId());
- return true;
- }
- return false;
- });
-
+ holder.mToolbarLastOpened.setVisibility(View.VISIBLE);
+ holder.mToolbarLiveNow.setVisibility(View.GONE);
+ holder.mToolbar.setOnMenuItemClickListener(item -> {
+ switch (item.getItemId()) {
+ case R.id.action_delete_deck:
+ // TODO(kash): Implement delete.
+ // mDB.deleteDeck(deck.getId());
+ return true;
+ }
+ return false;
+ });
+ holder.mThumb.setOnClickListener(v -> {
+ Log.d(TAG, "Clicking through to PresentationActivity.");
+ String sessionId;
+ try {
+ sessionId = mDB.createSession(deck.getId());
+ } catch (VException e) {
+ handleError(v.getContext(), "Could not view deck.", e);
+ return;
+ }
+ startPresentationActivity(v.getContext(), sessionId);
+ });
+ }
holder.mToolbarTitle.setText(deck.getTitle());
holder.mThumb.setImageBitmap(deck.getThumb());
- holder.mThumb.setOnClickListener(v -> {
- Log.d(TAG, "Clicking through to PresentationActivity.");
- String sessionId;
- try {
- sessionId = mDB.createSession(deck.getId());
- } catch (VException e) {
- handleError(v.getContext(), "Could not view deck.", e);
- return;
- }
- startPresentationActivity(v.getContext(), sessionId);
- });
}
@Override
public int getItemCount() {
- return mDecks.getItemCount();
+ return mLiveDecks.getItemCount() + mDecks.getItemCount();
}
@Override
@@ -131,6 +154,36 @@
}
}
+ /**
+ * Offsets the position notifications from mDecks by the size of mLiveDecks.
+ */
+ private class OffsetListener implements ListListener {
+ @Override
+ public void notifyDataSetChanged() {
+ DeckListAdapter.this.notifyDataSetChanged();
+ }
+
+ @Override
+ 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);
+ }
+
+ @Override
+ public void onError(Exception e) {
+ DeckListAdapter.this.onError(e);
+ }
+ }
+
private void startPresentationActivity(Context ctx, String sessionId) {
Intent intent = new Intent(ctx, PresentationActivity.class);
intent.putExtra(PresentationActivity.SESSION_ID_KEY, sessionId);
diff --git a/android/app/src/main/java/io/v/syncslides/PresentationActivity.java b/android/app/src/main/java/io/v/syncslides/PresentationActivity.java
index b013f7a..228f7fc 100644
--- a/android/app/src/main/java/io/v/syncslides/PresentationActivity.java
+++ b/android/app/src/main/java/io/v/syncslides/PresentationActivity.java
@@ -12,8 +12,16 @@
import android.view.View;
import android.widget.Toast;
+import java.util.UUID;
+
import io.v.syncslides.db.DB;
+import io.v.syncslides.discovery.PresentationDiscovery;
+import io.v.syncslides.discovery.RpcPresentationDiscovery;
+import io.v.syncslides.model.Deck;
+import io.v.syncslides.model.Person;
+import io.v.syncslides.model.PresentationAdvertisement;
import io.v.syncslides.model.Session;
+import io.v.v23.context.VContext;
import io.v.v23.verror.VException;
/**
@@ -155,6 +163,29 @@
transaction.commit();
}
+ public void startPresentation() {
+ try {
+ // TODO(kash): Move this into Session so that the advertisement happens automatically
+ // when the session is loaded. This is a big hack just so I can test advertise/scan
+ // functionality.
+ String presentationId = UUID.randomUUID().toString();
+ Person person = new Person(
+ V23.Singleton.get().getBlessings().toString(),
+ SignInActivity.getUserName(this));
+ Deck deck = DB.Singleton.get().getDeck(mSession.getDeckId());
+ PresentationAdvertisement advertisement = new PresentationAdvertisement(
+ presentationId, person, deck, "dummy syncgroup name");
+ V23 v23 = V23.Singleton.get();
+ // TODO(kash): Session needs to stop advertising.
+ VContext advertiseContext = v23.getVContext().withCancel();
+ RpcPresentationDiscovery discovery = new RpcPresentationDiscovery(v23.getVContext());
+ discovery.advertise(advertiseContext, advertisement);
+ } catch (VException e) {
+ handleError("Could not start presentation", e);
+ return;
+ }
+ }
+
private void handleError(String msg, Throwable throwable) {
Log.e(TAG, msg + ": " + Log.getStackTraceString(throwable));
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
diff --git a/android/app/src/main/java/io/v/syncslides/SlideListFragment.java b/android/app/src/main/java/io/v/syncslides/SlideListFragment.java
index 9c382c3..a967967 100644
--- a/android/app/src/main/java/io/v/syncslides/SlideListFragment.java
+++ b/android/app/src/main/java/io/v/syncslides/SlideListFragment.java
@@ -66,16 +66,14 @@
// If there is not already a presentation for this session,
// make the FAB visible so that clicking it will start a new
// presentation.
- if (mSession.getPresentationId() == null) {
+ if (mSession.getPresentationId() == null || mSession.getPresentationId().equals("")) {
final FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(
R.id.play_presentation_fab);
fab.setVisibility(View.VISIBLE);
fab.setOnClickListener(v -> {
- // TODO(kash): Implement me.
-// mRole = Role.PRESENTER;
-// fab.setVisibility(View.INVISIBLE);
-// PresentationActivity activity = (PresentationActivity) v.getContext();
-// activity.startPresentation();
+ fab.setVisibility(View.INVISIBLE);
+ PresentationActivity activity = (PresentationActivity) v.getContext();
+ activity.startPresentation();
});
}
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.slide_list);
diff --git a/android/app/src/main/java/io/v/syncslides/db/DB.java b/android/app/src/main/java/io/v/syncslides/db/DB.java
index f437455..1e9f2e3 100644
--- a/android/app/src/main/java/io/v/syncslides/db/DB.java
+++ b/android/app/src/main/java/io/v/syncslides/db/DB.java
@@ -73,4 +73,6 @@
*/
ListenableFuture<Void> importDeck(Deck deck, Slide[] slides);
+ // TODO(kash): Remove this when moving advertisement functionality into Session.
+ Deck getDeck(String deckId) throws VException;
}
diff --git a/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java b/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java
index a319459..6047a77 100644
--- a/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java
+++ b/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java
@@ -33,6 +33,7 @@
import io.v.syncslides.InitException;
import io.v.syncslides.V23;
import io.v.syncslides.model.Deck;
+import io.v.syncslides.model.DeckImpl;
import io.v.syncslides.model.DynamicList;
import io.v.syncslides.model.NoopList;
import io.v.syncslides.model.Session;
@@ -207,6 +208,14 @@
});
}
+ @Override
+ public Deck getDeck(String deckId) throws VException {
+ Table decks = mDB.getTable(SyncbaseDB.DECKS_TABLE);
+ VDeck vDeck = (VDeck) sync(decks.get(mVContext, deckId, VDeck.class));
+ return new DeckImpl(vDeck.getTitle(), vDeck.getThumbnail(), deckId);
+ }
+
+
private void putDeck(Deck deck) throws VException {
Log.i(TAG, String.format("Adding deck %s, %s", deck.getId(), deck.getTitle()));
Table decks = mDB.getTable(DECKS_TABLE);
diff --git a/android/app/src/main/java/io/v/syncslides/discovery/ClientFactory.java b/android/app/src/main/java/io/v/syncslides/discovery/ClientFactory.java
new file mode 100644
index 0000000..4396ef0
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/discovery/ClientFactory.java
@@ -0,0 +1,12 @@
+// 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.syncslides.discovery;
+
+/**
+ * Makes clients to LivePresentation services.
+ */
+interface ClientFactory {
+ LivePresentationClient make(String name);
+}
diff --git a/android/app/src/main/java/io/v/syncslides/discovery/PresentationDiscovery.java b/android/app/src/main/java/io/v/syncslides/discovery/PresentationDiscovery.java
index b9357ee..b7a9c29 100644
--- a/android/app/src/main/java/io/v/syncslides/discovery/PresentationDiscovery.java
+++ b/android/app/src/main/java/io/v/syncslides/discovery/PresentationDiscovery.java
@@ -7,6 +7,7 @@
import io.v.syncslides.model.DynamicList;
import io.v.syncslides.model.PresentationAdvertisement;
import io.v.v23.context.VContext;
+import io.v.v23.verror.VException;
/**
* Handles advertising and scanning for live presentations.
@@ -24,5 +25,5 @@
* to stop advertising.
* @param advertisement details of the presentation.
*/
- void advertise(VContext vContext, PresentationAdvertisement advertisement);
+ void advertise(VContext vContext, PresentationAdvertisement advertisement) throws VException;
}
diff --git a/android/app/src/main/java/io/v/syncslides/discovery/RpcAdvertiser.java b/android/app/src/main/java/io/v/syncslides/discovery/RpcAdvertiser.java
new file mode 100644
index 0000000..47d5caf
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/discovery/RpcAdvertiser.java
@@ -0,0 +1,81 @@
+// 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.syncslides.discovery;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.android.v23.V;
+import io.v.syncslides.db.VDeck;
+import io.v.syncslides.db.VPerson;
+import io.v.syncslides.model.Deck;
+import io.v.syncslides.model.Person;
+import io.v.syncslides.model.PresentationAdvertisement;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.discovery.Service;
+import io.v.v23.discovery.VDiscovery;
+import io.v.v23.naming.Endpoint;
+import io.v.v23.rpc.Server;
+import io.v.v23.rpc.ServerCall;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.VSecurity;
+import io.v.v23.verror.VException;
+
+/**
+ * Advertises a live presentation using Vanadium Discovery. Additionally, it starts an
+ * RPC service to provide extra details about the presentation to scanners.
+ */
+class RpcAdvertiser {
+ private static final List<BlessingPattern> NO_PATTERNS = new ArrayList<>();
+ private static final String NO_MOUNT = "";
+
+ private final VContext mVContext;
+ private final VDiscovery mDiscovery;
+ private final PresentationAdvertisement mAdvertisement;
+
+ RpcAdvertiser(VContext vContext, VDiscovery discovery,
+ PresentationAdvertisement advertisement) {
+ mVContext = vContext;
+ mDiscovery = discovery;
+ mAdvertisement = advertisement;
+ }
+
+ void start() throws VException {
+ Server server = V.getServer(
+ V.withNewServer(
+ mVContext, NO_MOUNT, new MyLivePresentationServer(),
+ VSecurity.newAllowEveryoneAuthorizer()));
+ List<String> addresses = new ArrayList<>();
+ for (Endpoint point : server.getStatus().getEndpoints()) {
+ addresses.add(point.toString());
+ }
+ // InstanceId and InstanceName are left unset.
+ Service service = new Service();
+ service.setAddrs(addresses);
+ service.setInterfaceName(RpcPresentationDiscovery.INTERFACE_NAME);
+ Attributes attrs = new Attributes();
+ // RpcScanner will filter out any advertisements that have the same device id
+ // because the user doesn't want to see his own advertisements.
+ attrs.put(RpcPresentationDiscovery.DEVICE_ID_ATTRIBUTE,
+ RpcPresentationDiscovery.DEVICE_ID);
+ service.setAttrs(attrs);
+ mDiscovery.advertise(mVContext, service, NO_PATTERNS);
+ }
+
+ private class MyLivePresentationServer implements LivePresentationServer {
+ @Override
+ public PresentationInfo getInfo(VContext ctx, ServerCall call) throws VException {
+ Person person = mAdvertisement.getPresenter();
+ VPerson vPerson = new VPerson(person.getBlessing(), person.getName());
+
+ Deck deck = mAdvertisement.getDeck();
+ VDeck vDeck = new VDeck(deck.getTitle(), deck.getThumbData());
+
+ return new PresentationInfo(
+ vPerson, deck.getId(), vDeck, mAdvertisement.getSyncgroupName());
+ }
+ }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/discovery/RpcClientFactory.java b/android/app/src/main/java/io/v/syncslides/discovery/RpcClientFactory.java
new file mode 100644
index 0000000..078f50c
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/discovery/RpcClientFactory.java
@@ -0,0 +1,12 @@
+// 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.syncslides.discovery;
+
+class RpcClientFactory implements ClientFactory {
+ @Override
+ public LivePresentationClient make(String name) {
+ return LivePresentationClientFactory.getLivePresentationClient(name);
+ }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/discovery/RpcPresentationDiscovery.java b/android/app/src/main/java/io/v/syncslides/discovery/RpcPresentationDiscovery.java
new file mode 100644
index 0000000..a29932e
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/discovery/RpcPresentationDiscovery.java
@@ -0,0 +1,48 @@
+// 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.syncslides.discovery;
+
+import java.util.UUID;
+
+import io.v.android.v23.V;
+import io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.PresentationAdvertisement;
+import io.v.v23.context.VContext;
+import io.v.v23.verror.VException;
+
+/**
+ * Uses a combination of Vanadium Discovery and RPC to advertise/scan for
+ * live presentations.
+ */
+public class RpcPresentationDiscovery implements PresentationDiscovery {
+
+ static final String INTERFACE_NAME = "v.io/syncslides/discovery.LivePresentation";
+
+ /**
+ * Passed to both RpcScanner and RpcAdvertiser to ensure that advertisements from this
+ * device don't show up in the scan.
+ */
+ static final String DEVICE_ID_ATTRIBUTE = "device_id";
+ static final String DEVICE_ID = UUID.randomUUID().toString();
+
+ private final VContext mContext;
+
+ public RpcPresentationDiscovery(VContext context) {
+ mContext = context;
+ }
+
+ @Override
+ public DynamicList<PresentationAdvertisement> scan() {
+ return new RpcScanner(mContext, V.getDiscovery(mContext), new RpcClientFactory());
+ }
+
+ @Override
+ public void advertise(VContext vContext, PresentationAdvertisement advertisement)
+ throws VException {
+ RpcAdvertiser advertiser =
+ new RpcAdvertiser(vContext, V.getDiscovery(vContext), advertisement);
+ advertiser.start();
+ }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/discovery/RpcScanner.java b/android/app/src/main/java/io/v/syncslides/discovery/RpcScanner.java
new file mode 100644
index 0000000..1656b60
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/discovery/RpcScanner.java
@@ -0,0 +1,173 @@
+// 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.syncslides.discovery;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import io.v.syncslides.db.VDeck;
+import io.v.syncslides.model.Deck;
+import io.v.syncslides.model.DeckImpl;
+import io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.ListListener;
+import io.v.syncslides.model.Person;
+import io.v.syncslides.model.PresentationAdvertisement;
+import io.v.v23.InputChannel;
+import io.v.v23.InputChannels;
+import io.v.v23.VIterable;
+import io.v.v23.context.CancelableVContext;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.discovery.Service;
+import io.v.v23.discovery.Update;
+import io.v.v23.discovery.VDiscovery;
+import io.v.v23.verror.VException;
+
+import static io.v.v23.VFutures.sync;
+
+/**
+ * Scans for live presentations using Vanadium Discovery. When it does find those advertisements,
+ * it issues an RPC to fetch the details. The scanning is started/stopped when the first/last
+ * listener is added/removed.
+ */
+class RpcScanner implements DynamicList<PresentationAdvertisement> {
+ private static final String TAG = "RpcScanner";
+ private static final String QUERY = "v.InterfaceName=\"" +
+ RpcPresentationDiscovery.INTERFACE_NAME + "\"";
+
+ private final VContext mBaseContext;
+ private final VDiscovery mDiscovery;
+ private final ClientFactory mClientFactory;
+ private final Set<ListListener> mListeners;
+ private final ExecutorService mExecutor;
+ private final List<PresentationAdvertisement> mElems;
+ private final Handler mHandler;
+ private CancelableVContext mCurrentContext;
+
+ public RpcScanner(VContext context, VDiscovery discovery, ClientFactory clientFactory) {
+ mBaseContext = context;
+ mDiscovery = discovery;
+ mClientFactory = clientFactory;
+ mListeners = Sets.newHashSet();
+ mExecutor = Executors.newSingleThreadExecutor();
+ mElems = Lists.newArrayList();
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public int getItemCount() {
+ return mElems.size();
+ }
+
+ @Override
+ public PresentationAdvertisement get(int i) {
+ return mElems.get(i);
+ }
+
+ @Override
+ public void addListener(final ListListener listener) {
+ mListeners.add(listener);
+ if (mListeners.size() == 1) {
+ // First listener. Start the thread.
+ mCurrentContext = mBaseContext.withCancel();
+ mExecutor.submit(() -> scan());
+ }
+ mHandler.post(() -> listener.notifyDataSetChanged());
+ }
+
+ @Override
+ public void removeListener(ListListener listener) {
+ mListeners.remove(listener);
+ if (mListeners.isEmpty()) {
+ // Stop the scan via cancel.
+ mCurrentContext.cancel();
+ mCurrentContext = null;
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ }
+
+ private void addAdvertisement(PresentationAdvertisement ad) {
+ mElems.add(ad);
+ for (ListListener listener : mListeners) {
+ listener.notifyItemInserted(mElems.size() - 1);
+ }
+ }
+
+ private void removeAdvertisement(String id) {
+ for (int i = 0; i < mElems.size(); i++) {
+ if (id.equals(mElems.get(i).getId())) {
+ mElems.remove(i);
+ for (ListListener listener : mListeners) {
+ listener.notifyItemRemoved(i);
+ }
+ }
+ }
+ }
+
+ private void handleError(Exception e) {
+ for (ListListener listener : mListeners) {
+ listener.onError(e);
+ }
+ }
+
+ // Runs in a background thread.
+ private void scan() {
+ InputChannel<Update> updateChannel = mDiscovery.scan(mCurrentContext, QUERY);
+ final VIterable<Update> updates = InputChannels.asIterable(updateChannel);
+ for (Update update : updates) {
+ if (update instanceof Update.Found) {
+ Service descriptor = ((Update.Found) update).getElem().getService();
+ Attributes attrs = descriptor.getAttrs();
+ String remoteDevice = attrs.get(RpcPresentationDiscovery.DEVICE_ID_ATTRIBUTE);
+ if (remoteDevice != null &&
+ remoteDevice.equals(RpcPresentationDiscovery.DEVICE_ID)) {
+ // Self advertisement.
+ continue;
+ }
+ fetchDetails(descriptor);
+ } else {
+ String id = ((Update.Lost) update).getElem().getInstanceId();
+ mHandler.post(() -> removeAdvertisement(id));
+ }
+ }
+ if (updates.error() != null) {
+ mHandler.post(() -> handleError(updates.error()));
+ }
+ }
+
+ // Runs in a background thread.
+ private void fetchDetails(Service descriptor) {
+ List<String> addresses = descriptor.getAddrs();
+ if (addresses.isEmpty()) {
+ Log.e(TAG, "Descriptor " + descriptor.getInstanceId() + " had no addrs.");
+ return;
+ }
+ String name = "/" + addresses.get(0);
+ LivePresentationClient client = mClientFactory.make(name);
+ VContext rpcContext = mCurrentContext.withCancel();
+ PresentationInfo info;
+ try {
+ info = sync(client.getInfo(rpcContext));
+ } catch (VException e) {
+ Log.e(TAG, "Failed to fetch presentation info: " + e.getMessage());
+ return;
+ }
+ VDeck vDeck = info.getDeck();
+ Deck deck = new DeckImpl(vDeck.getTitle(), vDeck.getThumbnail(), info.getDeckId());
+ Person person = new Person(info.getPerson().getBlessing(), info.getPerson().getName());
+ final PresentationAdvertisement ad = new PresentationAdvertisement(
+ descriptor.getInstanceId(), person, deck, info.getSyncgroupName());
+ mHandler.post(() -> addAdvertisement(ad));
+ }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/discovery/presentation.vdl b/android/app/src/main/java/io/v/syncslides/discovery/presentation.vdl
new file mode 100644
index 0000000..5322535
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/discovery/presentation.vdl
@@ -0,0 +1,20 @@
+// 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 discovery
+
+import (
+ "io/v/syncslides/db"
+)
+
+type PresentationInfo struct {
+ Person db.VPerson
+ DeckId string
+ Deck db.VDeck
+ SyncgroupName string
+}
+
+type LivePresentation interface {
+ GetInfo() (PresentationInfo | error)
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/v/syncslides/model/PresentationAdvertisement.java b/android/app/src/main/java/io/v/syncslides/model/PresentationAdvertisement.java
index 4271746..4bc39b6 100644
--- a/android/app/src/main/java/io/v/syncslides/model/PresentationAdvertisement.java
+++ b/android/app/src/main/java/io/v/syncslides/model/PresentationAdvertisement.java
@@ -9,17 +9,26 @@
* member could choose to join it.
*/
public class PresentationAdvertisement {
- Person mPresenter;
- Deck mDeck;
- String mSyncgroupName;
+ private final String mId;
+ private final Person mPresenter;
+ private final Deck mDeck;
+ private final String mSyncgroupName;
- public PresentationAdvertisement(Person presenter, Deck deck, String syncgroupName) {
+ public PresentationAdvertisement(String id, Person presenter, Deck deck, String syncgroupName) {
+ mId = id;
mPresenter = presenter;
mDeck = deck;
mSyncgroupName = syncgroupName;
}
/**
+ * Returns the unique ID for this presentation.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
* Returns the person who is presenting.
*/
public Person getPresenter() {
@@ -39,4 +48,16 @@
public String getSyncgroupName() {
return mSyncgroupName;
}
+
+ /**
+ * Detects equality with the id passed to the constructor.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof PresentationAdvertisement) {
+ PresentationAdvertisement other = (PresentationAdvertisement) o;
+ return mId.equals(other.mId);
+ }
+ return false;
+ }
}
diff --git a/android/app/src/main/res/layout/deck_card.xml b/android/app/src/main/res/layout/deck_card.xml
index 66302b8..8d0cb5a 100644
--- a/android/app/src/main/res/layout/deck_card.xml
+++ b/android/app/src/main/res/layout/deck_card.xml
@@ -41,6 +41,7 @@
<TextView
android:id="@+id/deck_card_toolbar_live_now"
style="@style/DeckLiveNowFont"
+ android:text="@string/presentation_live"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/toolbar_text_top_margin"
diff --git a/dart/Makefile b/dart/Makefile
index 6ae7cd9..b850d95 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -3,10 +3,9 @@
endif
SYNCBASE_DATA_DIR=/data/data/org.chromium.mojo.shell/app_home/syncbasedata
-# Mounttable address on SyncSlides-Alpha network
-# TODO(aghassemi): Now that BLE discovery support is added and sync uses neighbourhood,
-# we should no longer need a mounttable after upgrading and testing the latest code.
-MOUNTTABLE_ADDR := /192.168.86.254:8101
+# TODO(aghassemi): We should be able to drop the mount table dependency once syncgroup creation no longer requires one.
+# See https://github.com/vanadium/issues/issues/873
+MOUNTTABLE_ADDR := /ns.dev.v.io:8101/tmp/syncslides
DEVICE_NUM_PLUS_ONE := $(shell echo $(DEVICE_NUM) \+ 1 | bc)
DEVICE_ID := $(shell adb devices | sed -n $(DEVICE_NUM_PLUS_ONE)p | awk '{ print $$1; }')
@@ -20,7 +19,8 @@
VLOG_FLAGS = --v=$(VLOG) --logtostderr=true
endif
-SYNCBASE_ARGS := https://syncbase.syncslides.mojo.v.io/syncbase_server.mojo --root-dir=$(SYNCBASE_DATA_DIR) --v23.namespace.root=$(MOUNTTABLE_ADDR) --name=$(DEVICE_ID) $(VLOG_FLAGS)
+# TODO(aghassemi): Remove use of proxy once BLE-RPC is supported.
+SYNCBASE_ARGS := https://syncbase.syncslides.mojo.v.io/syncbase_server.mojo --root-dir=$(SYNCBASE_DATA_DIR) --v23.proxy=/ns.dev.v.io:8101/proxy --v23.namespace.root=$(MOUNTTABLE_ADDR) --name=$(DEVICE_ID) $(VLOG_FLAGS)
SETTINGS_FILE := /sdcard/syncslides_settings.json
SETTINGS_JSON := {\"deviceid\": \"$(DEVICE_ID)\", \"mounttable\": \"$(MOUNTTABLE_ADDR)\"}
diff --git a/dart/lib/discovery/client.dart b/dart/lib/discovery/client.dart
index 87b2d47..1274f86 100644
--- a/dart/lib/discovery/client.dart
+++ b/dart/lib/discovery/client.dart
@@ -49,8 +49,8 @@
serviceAttrs['deckid'] = presentation.deck.key;
serviceAttrs['name'] = presentation.deck.name;
serviceAttrs['thumbnailkey'] = presentation.deck.thumbnail.key;
+ serviceAttrs['presentationid'] = presentation.key;
v23discovery.Service serviceInfo = new v23discovery.Service()
- ..instanceId = presentation.key
..interfaceName = presentationInterfaceName
..instanceName = presentation.key
..attrs = serviceAttrs
@@ -63,7 +63,12 @@
_advertiseCalls[presentation.key] =
new ProxyResponseFuturePair(advertiser, advertiseResponseFuture);
- await advertiseResponseFuture;
+ v23discovery.AdvertiserAdvertiseResponseParams result =
+ await advertiseResponseFuture;
+ if (result.err != null) {
+ throw result.err;
+ }
+
log.info('Advertised ${presentation.deck.name} under ${presentation.key}.');
}
@@ -119,7 +124,10 @@
var scannerResponseFuture = scanner.ptr.scan(query, handlerStub);
_scanCall = new ProxyResponseFuturePair(scanner, scannerResponseFuture);
- await scannerResponseFuture;
+ v23discovery.ScannerScanResponseParams result = await scannerResponseFuture;
+ if (result.err != null) {
+ throw result.err;
+ }
log.info('Scan started.');
}
@@ -156,13 +164,15 @@
}
class ScanHandler extends v23discovery.ScanHandler {
+ Map<String, String> instanceIdToPresentationIdMap = new Map();
found(v23discovery.Service s) async {
- String key = s.instanceId;
+ String key = s.attrs['presentationid'];
+ instanceIdToPresentationIdMap[s.instanceId] = key;
log.info('Found presentation ${s.attrs['name']} under $key.');
// Ignore our own advertised services.
if (_advertiseCalls.containsKey(key)) {
log.info(
- 'Presentation ${s.attrs['name']} was advertised by this device itself, ignoring it.');
+ 'Presentation ${s.attrs['name']} was advertised by us; ignoring.');
return;
}
@@ -177,8 +187,11 @@
_onFoundEmitter.add(presentation);
}
- lost(String presentationId) {
- // Ignore our own advertised services.
+ lost(String instanceId) {
+ String presentationId = instanceIdToPresentationIdMap[instanceId];
+ if (presentationId == null) {
+ return;
+ }
log.info('Lost presentation $presentationId.');
_onLostEmitter.add(presentationId);
}
diff --git a/dart/lib/stores/state.dart b/dart/lib/stores/state.dart
index 34599c5..8261dfb 100644
--- a/dart/lib/stores/state.dart
+++ b/dart/lib/stores/state.dart
@@ -17,9 +17,8 @@
// List of decks.
UnmodifiableMapView<String, DeckState> get decks;
- // List of presentations advertised by this instance of the app.
- UnmodifiableListView<
- model.PresentationAdvertisement> get advertisedPresentations;
+ // The presentation advertised by this instance of the app.
+ model.PresentationAdvertisement get advertisedPresentation;
// List of presentations advertised by others.
UnmodifiableMapView<String,
diff --git a/dart/lib/stores/syncbase/actions.dart b/dart/lib/stores/syncbase/actions.dart
index 040700f..a266586 100644
--- a/dart/lib/stores/syncbase/actions.dart
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -73,17 +73,15 @@
Future<model.PresentationAdvertisement> startPresentation(
String deckId) async {
- var alreadyAdvertised = _state._advertisedPresentations
- .any((model.PresentationAdvertisement p) => p.deck.key == deckId);
- if (alreadyAdvertised) {
- throw new ArgumentError.value(deckId,
- 'Cannot simultaneously present the same deck. Presentation already in progress.');
- }
-
if (!_state._decks.containsKey(deckId)) {
throw new ArgumentError.value(deckId, 'Deck no longer exists.');
}
+ // Stop the existing presentation, if any.
+ if (_state._advertisedPresentation != null) {
+ await stopPresentation(_state._advertisedPresentation.key);
+ }
+
model.Deck deck = _state._getOrCreateDeckState(deckId)._deck;
String presentationId = uuidutil.createUuid();
String syncgroupName = _getSyncgroupName(_state.settings, presentationId);
@@ -110,7 +108,7 @@
]);
await discovery.advertise(presentation);
- _state._advertisedPresentations.add(presentation);
+ _state._advertisedPresentation = presentation;
// Set the presentation state for the deck.
_DeckState deckState = _state._getOrCreateDeckState(deckId);
@@ -155,7 +153,7 @@
// Wait until at least the current slide number, driver and the slide for current slide number is synced.
join() async {
bool isMyOwnPresentation =
- _state._advertisedPresentations.any((p) => p.key == presentation.key);
+ _state._advertisedPresentation?.key == presentation.key;
if (!isMyOwnPresentation) {
await sb.joinSyncgroup(presentation.syncgroupName);
}
@@ -189,7 +187,7 @@
Future stopPresentation(String presentationId) async {
await discovery.stopAdvertising(presentationId);
- _state._advertisedPresentations.removeWhere((p) => p.key == presentationId);
+ _state._advertisedPresentation = null;
_state._decks.values.forEach((_DeckState deck) {
if (deck.presentation != null &&
deck.presentation.key == presentationId) {
diff --git a/dart/lib/stores/syncbase/state.dart b/dart/lib/stores/syncbase/state.dart
index 484a014..b943899 100644
--- a/dart/lib/stores/syncbase/state.dart
+++ b/dart/lib/stores/syncbase/state.dart
@@ -8,7 +8,8 @@
model.User get user => _user;
model.Settings get settings => _settings;
UnmodifiableMapView<String, DeckState> decks;
- UnmodifiableListView<model.PresentationAdvertisement> advertisedPresentations;
+ model.PresentationAdvertisement get advertisedPresentation =>
+ _advertisedPresentation;
UnmodifiableMapView<String,
model.PresentationAdvertisement> presentationAdvertisements;
@@ -16,8 +17,6 @@
_user = null;
_settings = null;
decks = new UnmodifiableMapView(_decks);
- advertisedPresentations =
- new UnmodifiableListView(_advertisedPresentations);
presentationAdvertisements =
new UnmodifiableMapView(_presentationsAdvertisements);
}
@@ -25,7 +24,7 @@
model.User _user;
model.Settings _settings;
Map<String, _DeckState> _decks = new Map();
- List<model.PresentationAdvertisement> _advertisedPresentations = new List();
+ model.PresentationAdvertisement _advertisedPresentation;
Map<String, model.PresentationAdvertisement> _presentationsAdvertisements =
new Map();
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index 8cef40f..1674050 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -238,7 +238,7 @@
syncbase:
description: syncbase
source: hosted
- version: "0.0.23"
+ version: "0.0.27"
test:
description: test
source: hosted
@@ -258,7 +258,7 @@
v23discovery:
description: v23discovery
source: hosted
- version: "0.0.7"
+ version: "0.0.9"
vector_math:
description: vector_math
source: hosted
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index 67ad8a3..f0cccde 100644
--- a/dart/pubspec.yaml
+++ b/dart/pubspec.yaml
@@ -5,8 +5,8 @@
path: "../../../../../flutter/packages/flutter"
logging: ">=0.11.2 <0.12.0"
mojo_services: ">=0.4.5 <0.5.0"
- syncbase: ">=0.0.23 <0.1.0"
- v23discovery: ">=0.0.4 < 0.1.0"
+ syncbase: ">=0.0.27 <0.1.0"
+ v23discovery: ">=0.0.9 < 0.1.0"
uuid: ">=0.5.0 <0.6.0"
dev_dependencies:
flutter_tools:
diff --git a/dart/shortcut_template b/dart/shortcut_template
index c553beb..008b5c3 100644
--- a/dart/shortcut_template
+++ b/dart/shortcut_template
@@ -1,4 +1,4 @@
---map-origin=http://flutter/=https://storage.googleapis.com/mojo/flutter/97bf8464d2c342a919a80949b7b43c403db6cf6c/android-arm/
+--map-origin=http://flutter/=https://storage.googleapis.com/mojo/flutter/90ef9fa39c36f4027b82e62262e5c0c43a0466a1/android-arm/
--url-mappings=mojo:flutter=http://flutter/flutter.mojo
--enable-multiprocess
--map-origin=https://syncbase.syncslides.mojo.v.io=https://%GS_BUCKET_URL%/