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%/