Merge "syncslides: Implement discovery (mostly)."
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 c56fe3e..afd5af9 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;
/**
@@ -139,6 +147,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"