Merge "android-lib: Add VBeam class for sharing over NFC."
diff --git a/benchmarks/rpcbench/build.gradle b/benchmarks/rpcbench/build.gradle
index b6a19d5..939196d 100644
--- a/benchmarks/rpcbench/build.gradle
+++ b/benchmarks/rpcbench/build.gradle
@@ -26,13 +26,6 @@
compile 'com.google.caliper:caliper:1.0-beta-2'
}
-task copyLib(type: Copy) {
- from([project(':lib').buildDir.getAbsolutePath(), 'libs', 'libv23.so'].join(File.separator))
- destinationDir = new File(['src', 'main', 'resources'].join(File.separator))
-}
-
-tasks.'processResources'.dependsOn(copyLib)
-
task wrapper(type: Wrapper) {
gradleVersion = '2.7'
}
@@ -44,3 +37,8 @@
vdl {
inputPaths += ['src/main/java']
}
+
+def libProject = rootProject.findProject(':lib')
+
+tasks.run.dependsOn libProject.tasks.copyVanadiumLib
+tasks.run.jvmArgs "-Djava.library.path=${libProject.buildDir}/libs"
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java b/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java
index 36b70b0..f3e3f26 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java
@@ -5,26 +5,34 @@
package io.v.moments.ifc;
import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.ListenableFuture;
/**
- * Something needing to advertise itself will want an implementation of this.
+ * Advertiser controls, intended to be similar to Scanner controls.
*/
public interface Advertiser {
/**
- * Asynchronously start advertising. Callback executed on success or
- * failure of advertising startup. The future returned on successful
- * startup should be given a callback to handle advertising shutdown.
+ * Asynchronously start advertising.
+ *
+ * Callbacks can be expected to run on the UX thread.
+ *
+ * @param startupCallback executed on success or failure of advertising
+ * startup.
+ * @param completionCallback executed on success or failure of advertising
+ * completion. An advertisement might shutdown
+ * for reasons other than a call to stop, e.g. a
+ * timeout.
*/
- void advertiseStart(FutureCallback<ListenableFuture<Void>> callback);
+ void start(FutureCallback<Void> startupCallback,
+ FutureCallback<Void> completionCallback);
/**
- * True if advertiseStop could usefully be called.
+ * True if stop could usefully be called.
*/
boolean isAdvertising();
/**
- * Synchronously stop advertising.
+ * Synchronously stop advertising. Should result in execution of
+ * completionCallback.
*/
- void advertiseStop();
+ void stop();
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java b/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java
new file mode 100644
index 0000000..b9084d9
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java
@@ -0,0 +1,43 @@
+// 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.moments.ifc;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import io.v.v23.InputChannelCallback;
+import io.v.v23.discovery.Update;
+
+/**
+ * Scanner controls, intended to be similar to Advertiser controls.
+ */
+public interface Scanner {
+ /**
+ * Asynchronously start scanning.
+ *
+ * Callbacks can be expected to run on the UX thread.
+ *
+ * @param startupCallback executed on success or failure of scan
+ * startup.
+ * @param updateCallback executed on each scan update (each found or
+ * lost advertisement).
+ * @param completionCallback executed on success or failure of scan
+ * completion. A scan might shutdown for reasons
+ * other than a call to stop, e.g. a timeout.
+ */
+ void start(FutureCallback<Void> startupCallback,
+ InputChannelCallback<Update> updateCallback,
+ FutureCallback<Void> completionCallback);
+
+ /**
+ * True if stop could usefully be called.
+ */
+ boolean isScanning();
+
+ /**
+ * Synchronously stop scanning. Should result in execution of
+ * completionCallback.
+ */
+ void stop();
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java b/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java
index 9d13e03..d627698 100644
--- a/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java
+++ b/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java
@@ -19,7 +19,6 @@
import io.v.android.libs.security.BlessingsManager;
import io.v.android.v23.V;
-import io.v.moments.ifc.ScanListener;
import io.v.v23.InputChannelCallback;
import io.v.v23.InputChannels;
import io.v.v23.context.VContext;
@@ -33,7 +32,10 @@
import io.v.v23.verror.VException;
/**
- * Various static V23 utilities gathered in an injectable class.
+ * Various static V23 utilities gathered into an instantiatable class.
+ *
+ * This allows v23 usage to be injected and mocked. The class is a singleton to
+ * avoid confusion about init, shutdown and the 'base context'.
*/
public class V23Manager {
private static final String TAG = "V23Manager";
@@ -47,50 +49,69 @@
private V23Manager() {
}
- public VContext advertise(final Service service, FutureCallback<ListenableFuture<Void>> callback) {
+ public void scan(
+ String query,
+ Duration duration,
+ FutureCallback<VContext> startupCallback,
+ InputChannelCallback<Update> updateCallback,
+ FutureCallback<Void> completionCallback) {
+ Log.d(TAG, "Starting scan with q=[" + query + "]");
if (mDiscovery == null) {
- callback.onFailure(new IllegalStateException("Discovery not ready."));
- return null;
+ startupCallback.onFailure(
+ new IllegalStateException("Discovery not ready."));
+ return;
}
- VContext context = mV23Ctx.withTimeout(Duration.standardMinutes(5));
- final ListenableFuture<ListenableFuture<Void>> fStart =
- mDiscovery.advertise(context, service, NO_PATTERNS);
- Futures.addCallback(fStart, callback);
- Log.d(TAG, "Back from V.getDiscovery.advertise");
- return context;
+ VContext context = contextWithTimeout(duration);
+ Futures.addCallback(
+ InputChannels.withCallback(
+ mDiscovery.scan(context, query), updateCallback),
+ completionCallback);
+ startupCallback.onSuccess(context);
}
- public VContext scan(String query, final ScanListener listener) {
+ public void advertise(
+ Service advertisement,
+ Duration duration,
+ FutureCallback<VContext> startupCallback,
+ FutureCallback<Void> completionCallback) {
if (mDiscovery == null) {
- Log.d(TAG, "Discovery not ready.");
- return null;
+ startupCallback.onFailure(
+ new IllegalStateException("Discovery not ready."));
+ return;
}
- VContext context = mV23Ctx.withCancel();
- Log.d(TAG, "Calling V.getDiscovery.scan with q=" + query);
- final ListenableFuture<Void> fStart =
- InputChannels.withCallback(mDiscovery.scan(context, query),
- new InputChannelCallback<Update>() {
- @Override
- public ListenableFuture<Void> onNext(Update result) {
- listener.scanUpdateReceived(result);
- return Futures.immediateFuture(null);
- }
- });
- Futures.addCallback(fStart, new FutureCallback<Void>() {
+ VContext context = contextWithTimeout(duration);
+ ListenableFuture<ListenableFuture<Void>> hey =
+ mDiscovery.advertise(context, advertisement, NO_PATTERNS);
+ Futures.addCallback(hey, makeWrapped(context, startupCallback, completionCallback));
+ }
+
+ /**
+ * This exists to allow the scan and advertise interfaces to behave the same
+ * way to clients, and to deliver the context via the "success" callback
+ * so there's no chance of a race condition between usage of that context
+ * and code in the callback.
+ */
+ private FutureCallback<ListenableFuture<Void>> makeWrapped(
+ final VContext context,
+ final FutureCallback<VContext> startup,
+ final FutureCallback<Void> completion) {
+ return new FutureCallback<ListenableFuture<Void>>() {
@Override
- public void onSuccess(Void result) {
- Log.d(TAG, "Scan started.");
+ public void onSuccess(ListenableFuture<Void> result) {
+ startup.onSuccess(context);
+ Futures.addCallback(result, completion);
}
@Override
- public void onFailure(Throwable t) {
- Log.d(TAG, "Failure to start scan.", t);
+ public void onFailure(final Throwable t) {
+ startup.onFailure(t);
}
- });
- return context;
+ };
}
- public synchronized void init(Activity activity, FutureCallback<Blessings> future) {
+
+ public synchronized void init(
+ Activity activity, FutureCallback<Blessings> blessingCallback) {
Log.d(TAG, "init");
if (mAndroidCtx != null) {
if (mAndroidCtx == activity.getApplicationContext()) {
@@ -108,7 +129,7 @@
Log.d(TAG, "Attempting to get blessings.");
ListenableFuture<Blessings> f = BlessingsManager.getBlessings(
mV23Ctx, activity, BLESSINGS_KEY, true);
- Futures.addCallback(f, future);
+ Futures.addCallback(f, blessingCallback);
try {
mDiscovery = V.newDiscovery(mV23Ctx);
} catch (VException e) {
@@ -126,7 +147,8 @@
mAndroidCtx = null;
}
- public VContext makeServerContext(String mountName, Object server) throws VException {
+ public VContext makeServerContext(
+ String mountName, Object server) throws VException {
return V.withNewServer(
mV23Ctx.withCancel(),
mountName,
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java
index f178285..5db8116 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java
@@ -5,6 +5,7 @@
package io.v.moments.model;
import android.graphics.Bitmap;
+import android.support.annotation.NonNull;
import android.util.Log;
import com.google.common.util.concurrent.FutureCallback;
@@ -30,10 +31,18 @@
/**
* Handles the advertising of a moment.
+ *
+ * Main role of class is to manage the advertising context and moment service
+ * lifetimes. This code too complex to leak into the UX code, and too moment
+ * specific to be part of v23Manager. This class should have no knowledge of UX,
+ * and UX should have no access to VContext, because said context is under
+ * active development, and it's too complex to cover possible interactions with
+ * tests. This approaches hides it all behind the simple Advertiser interface.
*/
public class AdvertiserImpl implements Advertiser {
static final String NO_MOUNT_NAME = "";
private static final String TAG = "AdvertiserImpl";
+
private final V23Manager mV23Manager;
private final Moment mMoment;
@@ -52,57 +61,104 @@
}
@Override
- public boolean isAdvertising() {
- return mAdvCtx != null || mServerCtx != null;
- }
-
- @Override
public String toString() {
return mMoment.getCaption();
}
@Override
- public void advertiseStart(FutureCallback<ListenableFuture<Void>> callback) {
- Log.d(TAG, "Entering advertiseStart.");
+ public void start(
+ FutureCallback<Void> startupCallback,
+ FutureCallback<Void> completionCallback) {
+ Log.d(TAG, "Entering start.");
if (isAdvertising()) {
- callback.onFailure(new IllegalStateException("Already advertising."));
+ startupCallback.onFailure(
+ new IllegalStateException("Already advertising."));
return;
}
- Log.d(TAG, "Starting service for moment " + mMoment);
try {
mServerCtx = mV23Manager.makeServerContext(
NO_MOUNT_NAME, new MomentServer());
} catch (VException e) {
mServerCtx = null;
- callback.onFailure(new IllegalStateException("Unable to start service.", e));
+ startupCallback.onFailure(
+ new IllegalStateException("Unable to start service.", e));
return;
}
- List<String> addresses = new ArrayList<>();
- Endpoint[] points = mV23Manager.getServer(mServerCtx).getStatus().getEndpoints();
- for (Endpoint point : points) {
- addresses.add(point.toString());
- }
- Attributes attrs = mMoment.makeAttributes();
- Log.d(TAG, "Starting advertisement of moment " + mMoment);
- Service service = makeAdvertisement(attrs, addresses);
- mAdvCtx = mV23Manager.advertise(service, callback);
- Log.d(TAG, "Exiting advertiseStart.");
+ mV23Manager.advertise(
+ makeAdvertisement(
+ mMoment.makeAttributes(), makeServerAddressList()),
+ Config.Discovery.DURATION,
+ makeWrapped(startupCallback), completionCallback);
+ Log.d(TAG, "Exiting start.");
}
@Override
- public void advertiseStop() {
- Log.d(TAG, "Entering advertiseStop");
+ public boolean isAdvertising() {
+ return mAdvCtx != null && !mAdvCtx.isCanceled();
+ }
+
+ @NonNull
+ private List<String> makeServerAddressList() {
+ List<String> addresses = new ArrayList<>();
+ Endpoint[] points = mV23Manager.getServer(
+ mServerCtx).getStatus().getEndpoints();
+ for (Endpoint point : points) {
+ addresses.add(point.toString());
+ }
+ return addresses;
+ }
+
+ /**
+ * A service must be started before advertising begins, which creates a
+ * cleanup problem should advertising fail to start. This callback accepts
+ * the advertising context on startup success, and cancels the already
+ * started service on failure. This wrapper necessary to cleanly kill the
+ * service should advertising fail to start. This callback feeds info to
+ * the startup callback, which presumably has some effect on UX.
+ */
+ private FutureCallback<VContext> makeWrapped(
+ final FutureCallback<Void> startup) {
+ return new FutureCallback<VContext>() {
+ @Override
+ public void onSuccess(VContext context) {
+ mAdvCtx = context;
+ startup.onSuccess(null);
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ mAdvCtx = null;
+ cancelService();
+ startup.onFailure(t);
+ }
+ };
+ }
+
+ @Override
+ public void stop() {
+ if (!isAdvertising()) {
+ throw new IllegalStateException("Not advertising.");
+ }
+ Log.d(TAG, "Entering stop");
if (mAdvCtx != null) {
Log.d(TAG, "Cancelling advertising.");
- mAdvCtx.cancel();
+ if (!mAdvCtx.isCanceled()) {
+ mAdvCtx.cancel();
+ }
mAdvCtx = null;
}
+ cancelService();
+ Log.d(TAG, "Exiting stop");
+ }
+
+ private void cancelService() {
if (mServerCtx != null) {
Log.d(TAG, "Cancelling service.");
- mServerCtx.cancel();
+ if (!mServerCtx.isCanceled()) {
+ mServerCtx.cancel();
+ }
mServerCtx = null;
}
- Log.d(TAG, "Exiting advertiseStop");
}
/**
@@ -114,7 +170,7 @@
return new Service(
mMoment.getId().toString(),
mMoment.toString(),
- Config.INTERFACE_NAME,
+ Config.Discovery.INTERFACE_NAME,
attrs,
addresses,
new Attachments());
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/Config.java b/projects/moments/app/src/main/java/io/v/moments/model/Config.java
index 1e1a678..8761a52 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/Config.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/Config.java
@@ -10,6 +10,8 @@
import android.os.Environment;
import android.os.Handler;
+import org.joda.time.Duration;
+
import java.io.File;
import io.v.moments.R;
@@ -22,19 +24,13 @@
// App can become slower as a result, e.g. a thumbnail transfer that would
// be quick might be competing for bandwidth with a full size photo.
public static final boolean DO_FULL_SIZE_TOO = true;
-
- // Parent directory to all app storage.
+ /**
+ * Parent directory to all app storage.
+ */
private static final File PHOTO_PARENT_DIR =
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
- // Required type/interface name, probably a URL into a web-based
- // ontology. Necessary for querying.
- static final String INTERFACE_NAME = "v.io/x/ref.Moments";
-
- // To limit scans to see only this service.
- public static final String QUERY = "v.InterfaceName=\"" + INTERFACE_NAME + "\"";
-
/**
* Returns purported image display area.
*
@@ -64,4 +60,24 @@
R.dimen.moment_image_width)
);
}
+
+ /** Constants related to discovery. */
+ public static class Discovery {
+ /**
+ * Required type/interface name, probably a URL into a web-based
+ * ontology. Necessary for querying.
+ */
+ public static final String INTERFACE_NAME = "v.io/x/ref.Moments";
+ /**
+ * To limit scans to see only this service.
+ */
+ public static final String QUERY = "v.InterfaceName=\"" + INTERFACE_NAME + "\"";
+
+ /**
+ * After this duration an advertisement or scan for an advertisement
+ * will automatically stop. Choice is arbitrary. A nice exercise would
+ * be to add this to a settings menu.
+ */
+ public static final Duration DURATION = Duration.standardMinutes(5);
+ }
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java
new file mode 100644
index 0000000..82f14f3
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java
@@ -0,0 +1,114 @@
+// 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.moments.model;
+
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import io.v.moments.ifc.Scanner;
+import io.v.moments.lib.V23Manager;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Update;
+
+/**
+ * Handles scanning for moments.
+ *
+ * This class similar to AdvertiserImpl - see that class for more commentary -
+ * in that it functions as decoupling of the UX from the moving target that is
+ * the V23/VContext API
+ *
+ * At the moment, the complexity is far less than it is for advertising
+ * (scanning doesn't require a running service), so this class doesn't add much
+ * value over using an instance of V23Manager and VContext directly in whatever
+ * class uses a Scanner. To make this class more useful in decoupling moments
+ * specific code from v23 specifics, this class could tease apart a scan Update,
+ * and feed advertisement data from the Update.Found and Update.Lost object
+ * directly into, say, the appropriate instances of java.util.function.Consumer<T>
+ * that were passed to #start en lieu of the InputChannelCallback<Update>
+ * updateCallback. That way the caller would have no exposure to v23 classes.
+ */
+public class ScannerImpl implements Scanner {
+ private static final String TAG = "ScannerImpl";
+ private final V23Manager mV23Manager;
+ private final String mQuery;
+ private VContext mScanCtx;
+
+ public ScannerImpl(V23Manager v23Manager, String query) {
+ if (v23Manager == null) {
+ throw new IllegalArgumentException("Null v23Manager.");
+ }
+ if (query == null || query.isEmpty()) {
+ throw new IllegalArgumentException("Empty query.");
+ }
+ mV23Manager = v23Manager;
+ mQuery = query;
+ }
+
+ @Override
+ public String toString() {
+ return "scan(" + mQuery + "," + isScanning() + ")";
+ }
+
+ /**
+ * Accepts the scanning context on success (or assures that it's null on
+ * failure), then activates the callback supplied by the client (likely to
+ * change the UX).
+ */
+ private FutureCallback<VContext> makeWrapped(
+ final FutureCallback<Void> startup) {
+ return new FutureCallback<VContext>() {
+ @Override
+ public void onSuccess(VContext context) {
+ mScanCtx = context;
+ startup.onSuccess(null);
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ mScanCtx = null;
+ startup.onFailure(t);
+ }
+ };
+ }
+
+ @Override
+ public void start(
+ FutureCallback<Void> startupCallback,
+ InputChannelCallback<Update> updateCallback,
+ FutureCallback<Void> completionCallback) {
+ Log.d(TAG, "Entering start.");
+ if (isScanning()) {
+ startupCallback.onFailure(
+ new IllegalStateException("Already scanning."));
+ return;
+ }
+ mV23Manager.scan(
+ mQuery, Config.Discovery.DURATION,
+ makeWrapped(startupCallback),
+ updateCallback,
+ completionCallback);
+ Log.d(TAG, "Exiting start.");
+ }
+
+ @Override
+ public boolean isScanning() {
+ return mScanCtx != null && !mScanCtx.isCanceled();
+ }
+
+ @Override
+ public void stop() {
+ Log.d(TAG, "Entering stop");
+ if (mScanCtx != null) {
+ Log.d(TAG, "Cancelling scan.");
+ if (!mScanCtx.isCanceled()) {
+ mScanCtx.cancel();
+ }
+ mScanCtx = null;
+ }
+ Log.d(TAG, "Exiting stop");
+ }
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/Toaster.java b/projects/moments/app/src/main/java/io/v/moments/model/Toaster.java
new file mode 100644
index 0000000..5e4e246
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/model/Toaster.java
@@ -0,0 +1,31 @@
+// Copyright 2016 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.moments.model;
+
+import android.app.Activity;
+import android.widget.Toast;
+
+/**
+ * Wrapper allowing limited access to an activity.
+ */
+public class Toaster {
+
+ private final Activity mActivity;
+
+ public Toaster(Activity activity) {
+ mActivity = activity;
+ }
+
+ public boolean isDestroyed() {
+ return mActivity.isDestroyed();
+ }
+
+ /**
+ * Must be called from UX thread.
+ */
+ public void toast(String msg) {
+ Toast.makeText(mActivity, msg, Toast.LENGTH_SHORT).show();
+ }
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java b/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
index 2e6e21b..f1a91c0 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
@@ -24,7 +24,6 @@
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
-import android.widget.CompoundButton;
import android.widget.Toast;
import com.google.common.util.concurrent.FutureCallback;
@@ -38,6 +37,7 @@
import io.v.moments.ifc.Advertiser;
import io.v.moments.ifc.Moment;
import io.v.moments.ifc.MomentFactory;
+import io.v.moments.ifc.Scanner;
import io.v.moments.lib.DiscoveredList;
import io.v.moments.lib.Id;
import io.v.moments.lib.ObservedList;
@@ -49,8 +49,9 @@
import io.v.moments.model.Config;
import io.v.moments.model.FileUtil;
import io.v.moments.model.MomentFactoryImpl;
+import io.v.moments.model.ScannerImpl;
import io.v.moments.model.StateStore;
-import io.v.v23.context.VContext;
+import io.v.moments.model.Toaster;
import io.v.v23.security.Blessings;
/**
@@ -77,8 +78,14 @@
* adding a (very large) photo to an advertisement as an attribute noticeably
* increases the time taken between clicking 'advertise' on one device and
* seeing any evidence of the advertisement on another device.
+ *
+ * TODO: when reloading from prefs, don't change advertise or scan state. only
+ * do that when reloading from bundle. TODO: ScannerImpl should handle the
+ * Update parsing currently done by DiscoveredList. TODO: unit tests. TODO:
+ * Add version number to prefs, ignore and overwrite state if old version (to
+ * avoid need to manually wipe data to avoid crashes).
*/
-public class MainActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener {
+public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// Android Marshmallow permissions list.
private static final String[] PERMS = {
@@ -104,13 +111,14 @@
// See wireUxToDataModel for discussion of the following.
private StateStore mStateStore;
private AdvertiserFactory mAdvertiserFactory;
+ private Scanner mScanner;
+ private ScanSwitchHolder mScanSwitchHolder;
private MomentFactory mMomentFactory;
private BitMapper mBitMapper;
private ObservedList<Moment> mLocalMoments;
private DiscoveredList<Moment> mRemoteMoments;
- private VContext mScanCtx;
- private boolean mShouldBeScanning;
private Id mCurrentPhotoId;
+ private boolean mShouldBeScanning = false;
/**
* The number used in a moment's file name is called the moments 'ordinal'
@@ -202,6 +210,12 @@
// represent local moments).
mRemoteMoments = new DiscoveredList<>(converter, mAdvertiserFactory, mHandler);
+ Toaster toaster = new Toaster(this);
+
+ mScanner = new ScannerImpl(mV23Manager, Config.Discovery.QUERY);
+ mScanSwitchHolder = new ScanSwitchHolder(
+ toaster, mScanner, mRemoteMoments);
+
// Stores app state to bundles, preferences, etc. The mMomentFactory
// needed to recreate moments.
mStateStore = new StateStore(
@@ -217,8 +231,7 @@
// for local moments when a user wants to advertise them. The
// serialExecutor is used to start/stop advertisements in the UX.
MomentAdapter adapter = new MomentAdapter(
- mRemoteMoments, mLocalMoments,
- mAdvertiserFactory, mHandler);
+ mRemoteMoments, mLocalMoments, toaster, mAdvertiserFactory);
// Lets the adapter speed up a bit.
adapter.setHasStableIds(true);
@@ -292,16 +305,7 @@
Config.getWorkingDirectory(this));
return;
}
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- Toast.makeText(
- MainActivity.this,
- R.string.need_permissions,
- Toast.LENGTH_LONG).show();
- finish();
- }
- });
+ toast(getString(R.string.need_permissions));
}
/**
@@ -329,9 +333,6 @@
Log.d(TAG, "Loading moments from prefs.");
mStateStore.prefsLoad(mLocalMoments);
}
- if (mShouldBeScanning && !isScanning()) {
- startScanning();
- }
}
@Override
@@ -344,11 +345,14 @@
protected void onDestroy() {
super.onDestroy();
logState("onDestroy");
- if (isScanning()) {
- stopScanning();
+ if (mScanner.isScanning()) {
+ mScanner.stop();
}
stopAllAdvertising();
+ mV23Manager.shutdown();
Log.d(TAG, "Destruction complete.");
+ Log.d(TAG, " ");
+ Log.d(TAG, " ");
}
private void stopAllAdvertising() {
@@ -356,7 +360,7 @@
for (Advertiser advertiser : mAdvertiserFactory.allAdvertisers()) {
if (advertiser.isAdvertising()) {
try {
- advertiser.advertiseStop();
+ advertiser.stop();
count++;
} catch (Exception e) {
e.printStackTrace();
@@ -366,10 +370,6 @@
Log.d(TAG, "A moment was not advertising");
}
}
- if (count > 0) {
- // This toast noisy if not debugging.
- // toast("Stopped " + count + " advertisements.");
- }
Log.d(TAG, "Stopped " + count + " advertisements.");
}
@@ -391,26 +391,6 @@
return mgr;
}
- private boolean isScanning() {
- return mScanCtx != null;
- }
-
- private Runnable setScanningSwitch(final boolean on) {
- return new Runnable() {
- @Override
- public void run() {
- SwitchCompat sw = (SwitchCompat) findViewById(R.id.action_scan);
- if (sw != null) {
- sw.setChecked(on);
- if (on) {
- toast("Started scanning.");
- } else {
- toast("Stopped scanning.");
- }
- }
- }
- };
- }
private void toast(final String msg) {
mHandler.post(new Runnable() {
@@ -421,61 +401,17 @@
});
}
- private void startScanning() {
- if (isScanning()) {
- throw new IllegalStateException("Already scanning.");
- }
- mSerialExecutor.submit(new Runnable() {
- @Override
- public void run() {
- Log.d(TAG, "Starting scan.");
- mScanCtx = mV23Manager.scan(Config.QUERY, mRemoteMoments);
- runOnUiThread(setScanningSwitch(true));
- Log.d(TAG, "Scan started.");
- }
- });
- }
-
- private void stopScanning() {
- if (!isScanning()) {
- throw new IllegalStateException("Not scanning.");
- }
- mSerialExecutor.submit(new Runnable() {
- @Override
- public void run() {
- Log.d(TAG, "Stopping scan.");
- mScanCtx.cancel();
- mScanCtx = null;
- mRemoteMoments.dropAll();
- runOnUiThread(setScanningSwitch(false));
- Log.d(TAG, "Scan stopped.");
- }
- });
- }
-
- @Override
- public void onCheckedChanged(CompoundButton button, boolean isChecked) {
- if (button.getId() != R.id.action_scan) {
- throw new IllegalStateException("Bad scan wiring.");
- }
- if (isChecked) {
- mShouldBeScanning = true;
- if (!isScanning()) {
- startScanning();
- }
- } else {
- mShouldBeScanning = false;
- if (isScanning()) {
- stopScanning();
- }
- }
- }
-
@Override
public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ logState("onCreateOptionsMenu");
getMenuInflater().inflate(R.menu.menu_main, menu);
MenuItem item = menu.findItem(R.id.action_scan);
- ((SwitchCompat) MenuItemCompat.getActionView(item)).setOnCheckedChangeListener(this);
+ SwitchCompat sw = (SwitchCompat) MenuItemCompat.getActionView(item);
+ mScanSwitchHolder.setSwitch(sw);
+ if (mShouldBeScanning && !sw.isChecked()) {
+ sw.setChecked(true);
+ }
return true;
}
@@ -511,7 +447,7 @@
protected void onSaveInstanceState(Bundle b) {
super.onSaveInstanceState(b);
logState("onSaveInstanceState");
- b.putBoolean(B.SHOULD_BE_SCANNING, mShouldBeScanning);
+ b.putBoolean(B.SHOULD_BE_SCANNING, mScanner.isScanning());
mStateStore.bundleSave(b, mRemoteMomentCache.values());
mStateStore.prefsSave(mLocalMoments);
}
@@ -520,7 +456,6 @@
mStateStore.prefsLoad(mLocalMoments);
if (b == null) {
Log.d(TAG, "No bundle passed, starting fresh.");
- mShouldBeScanning = false;
mRemoteMomentCache.clear();
return;
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java b/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
index 4edd069..3950771 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
@@ -16,6 +16,7 @@
import io.v.moments.ifc.Moment;
import io.v.moments.lib.ObservedList;
import io.v.moments.model.AdvertiserFactory;
+import io.v.moments.model.Toaster;
/**
* Stacks two moment lists in a recycler view.
@@ -24,17 +25,17 @@
implements ListObserver {
private final ObservedList<Moment> mRemoteMoments;
private final ObservedList<Moment> mLocalMoments;
+ private final Toaster mToaster;
private final AdvertiserFactory mAdvertiserFactory;
- private final Handler mHandler;
public MomentAdapter(ObservedList<Moment> remoteMoments,
ObservedList<Moment> localMoments,
- AdvertiserFactory advertiserFactory,
- Handler handler) {
+ Toaster toaster,
+ AdvertiserFactory advertiserFactory) {
mRemoteMoments = remoteMoments;
mLocalMoments = localMoments;
+ mToaster = toaster;
mAdvertiserFactory = advertiserFactory;
- mHandler = handler;
}
public void beginObserving() {
@@ -67,7 +68,7 @@
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.item_moment, parent, false);
- return new MomentHolder(view, context, mHandler);
+ return new MomentHolder(view, context, mToaster);
}
private boolean isRemote(int position) {
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java b/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
index 1227608..e172f1e 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
@@ -6,7 +6,6 @@
import android.content.Context;
import android.content.Intent;
-import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SwitchCompat;
import android.util.Log;
@@ -17,14 +16,15 @@
import android.widget.Toast;
import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.CancellationException;
import io.v.moments.R;
import io.v.moments.ifc.Advertiser;
import io.v.moments.ifc.Moment;
import io.v.moments.ifc.Moment.Kind;
import io.v.moments.ifc.Moment.Style;
+import io.v.moments.model.Toaster;
import static io.v.moments.ifc.Moment.AdState;
@@ -33,21 +33,21 @@
*/
public class MomentHolder extends RecyclerView.ViewHolder {
private static final String TAG = "MomentHolder";
- private final TextView authorTextView;
- private final TextView captionTextView;
- private final SwitchCompat advertiseButton;
- private final ImageView imageView;
+ private final TextView mAuthorTextView;
+ private final TextView mCaptionTextView;
+ private final SwitchCompat mAdvertiseButton;
+ private final ImageView mImageView;
private final Context mContext;
- private final Handler mHandler;
+ private final Toaster mToaster;
- public MomentHolder(View itemView, Context context, Handler handler) {
+ public MomentHolder(View itemView, Context context, Toaster toaster) {
super(itemView);
mContext = context;
- mHandler = handler;
- authorTextView = (TextView) itemView.findViewById(R.id.moment_author);
- captionTextView = (TextView) itemView.findViewById(R.id.moment_caption);
- advertiseButton = (SwitchCompat) itemView.findViewById(R.id.advertise_button);
- imageView = (ImageView) itemView.findViewById(R.id.moment_image);
+ mAuthorTextView = (TextView) itemView.findViewById(R.id.moment_author);
+ mCaptionTextView = (TextView) itemView.findViewById(R.id.moment_caption);
+ mAdvertiseButton = (SwitchCompat) itemView.findViewById(R.id.advertise_button);
+ mImageView = (ImageView) itemView.findViewById(R.id.moment_image);
+ mToaster = toaster;
}
/**
@@ -56,11 +56,11 @@
public void bind(Moment moment, Advertiser advertiser) {
final Kind kind = advertiser == null ? Kind.REMOTE : Kind.LOCAL;
Log.d(TAG, "Binding " + kind + " " + moment);
- authorTextView.setText(moment.getAuthor());
- captionTextView.setText(moment.getCaption());
+ mAuthorTextView.setText(moment.getAuthor());
+ mCaptionTextView.setText(moment.getCaption());
if (moment.hasPhoto(kind, Style.THUMB)) {
- imageView.setImageBitmap(moment.getPhoto(kind, Style.THUMB));
- imageView.setOnClickListener(showPhoto(moment, kind));
+ mImageView.setImageBitmap(moment.getPhoto(kind, Style.THUMB));
+ mImageView.setOnClickListener(showPhoto(moment, kind));
}
if (moment.hasPhoto(kind, Style.FULL)) {
if (!moment.hasPhoto(kind, Style.THUMB)) {
@@ -69,14 +69,14 @@
}
}
if (kind.equals(Kind.REMOTE)) {
- advertiseButton.setVisibility(View.INVISIBLE);
+ mAdvertiseButton.setVisibility(View.INVISIBLE);
} else {
- advertiseButton.setText("");
- advertiseButton.setVisibility(View.VISIBLE);
- advertiseButton.setEnabled(true);
- advertiseButton.setOnCheckedChangeListener(
+ mAdvertiseButton.setText("");
+ mAdvertiseButton.setVisibility(View.VISIBLE);
+ mAdvertiseButton.setEnabled(true);
+ mAdvertiseButton.setOnCheckedChangeListener(
toggleAdvertising(moment, advertiser));
- advertiseButton.setChecked(
+ mAdvertiseButton.setChecked(
moment.getDesiredAdState().equals(AdState.ON));
}
}
@@ -103,99 +103,80 @@
@Override
public void onCheckedChanged(CompoundButton button, boolean isChecked) {
if (isChecked) {
- handleStartAdvertising(moment, advertiser);
+ if (!advertiser.isAdvertising()) {
+ advertiser.start(
+ makeAdvertiseStartCallback(moment),
+ makeAdvertiseStopCallback(moment));
+ } else {
+ Log.d(TAG, "Advertiser already on.");
+ }
} else {
- handleStopAdvertising(moment, advertiser);
+ if (advertiser.isAdvertising()) {
+ advertiser.stop();
+ } else {
+ Log.d(TAG, "Advertiser already off.");
+ }
}
}
};
}
- private void handleStartAdvertising(final Moment moment, final Advertiser advertiser) {
- moment.setDesiredAdState(AdState.ON);
- advertiser.advertiseStart(makeAdvertiseCallback(moment));
- }
-
- private void handleStopAdvertising(final Moment moment, final Advertiser advertiser) {
- moment.setDesiredAdState(AdState.OFF);
- if (advertiser.isAdvertising()) {
- advertiser.advertiseStop();
- toast("Stopped advertising " + moment.getCaption());
- } else {
- Log.d(TAG, "handleStopAdvertising called, but not advertising.");
- }
- }
-
- private FutureCallback<ListenableFuture<Void>> makeAdvertiseCallback(final Moment moment) {
- return new FutureCallback<ListenableFuture<Void>>() {
- private void assureStopped() {
- moment.setDesiredAdState(AdState.OFF);
- if (advertiseButton.isChecked()) {
- advertiseButton.setChecked(false);
- }
- }
+ private FutureCallback<Void> makeAdvertiseStartCallback(final Moment moment) {
+ return new FutureCallback<Void>() {
@Override
- public void onSuccess(ListenableFuture<Void> result) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- moment.setDesiredAdState(AdState.ON);
- advertiseButton.setChecked(true);
- }
- });
- Futures.addCallback(
- result, new FutureCallback<Void>() {
- @Override
- public void onSuccess(Void result) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- assureStopped();
- }
- });
- }
-
- @Override
- public void onFailure(final Throwable t) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- assureStopped();
- if (t instanceof java.util.concurrent.CancellationException) {
- // At the time of writing, the only way advertising ends
- // is by throwing this exception, so this is actually
- // a non-exceptional success case.
- } else {
- Log.d(TAG, "Failure to gracefully stop advertising.", t);
-
- }
- }
- });
- }
- }
- );
+ public void onSuccess(Void result) {
+ Log.d(TAG, "start:onSuccess:" + moment);
+ moment.setDesiredAdState(AdState.ON);
+ if (!mAdvertiseButton.isChecked()) {
+ mAdvertiseButton.setChecked(true);
+ }
+ mToaster.toast("Advertising " + moment.getCaption());
}
@Override
public void onFailure(final Throwable t) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- assureStopped();
- Log.d(TAG, "Failure to start advertising " + moment, t);
- }
- });
+ cleanUpPostStop(moment);
+ mToaster.toast("Failure to start advertising " + moment.getCaption());
+ Log.d(TAG, "Failure to start advertising " + moment, t);
}
};
}
- private void toast(final String msg) {
- mHandler.post(new Runnable() {
+ /**
+ * Verify that advertising is off and that the UX reflects that fact.
+ */
+ private void cleanUpPostStop(final Moment moment) {
+ Log.d(TAG, "cleanUpPostStop");
+ if (mToaster.isDestroyed()) {
+ Log.d(TAG, "The activity is dead, no UX to fix.");
+ return;
+ }
+ moment.setDesiredAdState(AdState.OFF);
+ if (mAdvertiseButton.isChecked()) {
+ Log.d(TAG, "Advertising off, must cleanup UX.");
+ mAdvertiseButton.setChecked(false);
+ }
+ }
+
+ private FutureCallback<Void> makeAdvertiseStopCallback(final Moment moment) {
+ return new FutureCallback<Void>() {
@Override
- public void run() {
- Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
+ public void onSuccess(Void result) {
+ cleanUpPostStop(moment);
}
- });
+
+ @Override
+ public void onFailure(final Throwable t) {
+ cleanUpPostStop(moment);
+ if (t instanceof CancellationException) {
+ // At the time of writing, the only way advertising ends
+ // is by throwing this exception, so this is actually
+ // a non-exceptional success case.
+ } else {
+ Log.d(TAG, "Failure to gracefully stop advertising.", t);
+ }
+ }
+ };
}
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java b/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java
new file mode 100644
index 0000000..c5187f4
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java
@@ -0,0 +1,153 @@
+// Copyright 2016 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.moments.ux;
+
+import android.app.Activity;
+import android.support.v7.widget.SwitchCompat;
+import android.util.Log;
+import android.widget.CompoundButton;
+import android.widget.Toast;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.CancellationException;
+
+import io.v.moments.ifc.Moment;
+import io.v.moments.ifc.Scanner;
+import io.v.moments.lib.DiscoveredList;
+import io.v.moments.model.Toaster;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.discovery.Update;
+
+/**
+ * Manages scanning UX.
+ *
+ * The callbacks provided will be run on the UX thread, so its safe to perform
+ * UX operations in the callbacks.
+ */
+public class ScanSwitchHolder implements CompoundButton.OnCheckedChangeListener {
+ private static final String TAG = "ScanSwitchHolder";
+ private final Scanner mScanner;
+ private final Toaster mToaster;
+ private final DiscoveredList<Moment> mRemoteMoments;
+ private SwitchCompat mSwitch;
+
+ public ScanSwitchHolder(
+ Toaster toaster, Scanner scanner,
+ DiscoveredList<Moment> remoteMoments) {
+ if (toaster == null) {
+ throw new IllegalArgumentException("Null activity.");
+ }
+ if (scanner == null) {
+ throw new IllegalArgumentException("Null scanner.");
+ }
+ if (remoteMoments == null) {
+ throw new IllegalArgumentException("Null remoteMoments.");
+ }
+ mToaster = toaster;
+ mScanner = scanner;
+ mRemoteMoments = remoteMoments;
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton button, boolean isChecked) {
+ Log.d(TAG, "onCheckedChanged with checked = " + isChecked);
+ if (button.getId() != mSwitch.getId()) {
+ throw new IllegalStateException("Bad scan wiring.");
+ }
+ if (isChecked) {
+ if (mScanner.isScanning()) {
+ Log.d(TAG, "Asked to start scanning, but already scanning.");
+ return;
+ }
+ mScanner.start(makeStartupCallback(), makeUpdateCallback(), makeCompletionCallback());
+ } else {
+ if (!mScanner.isScanning()) {
+ Log.d(TAG, "Asked to stop scanning, but already not scanning.");
+ return;
+ }
+ mScanner.stop();
+ }
+ }
+
+ public void setSwitch(SwitchCompat theSwitch) {
+ mSwitch = theSwitch;
+ theSwitch.setOnCheckedChangeListener(this);
+ }
+
+ private FutureCallback<Void> makeStartupCallback() {
+ return new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
+ Log.d(TAG, "Started scanning.");
+ mToaster.toast("Scanning.");
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.d(TAG, "Unable to scan.", t);
+ if (mSwitch.isChecked()) {
+ Log.d(TAG, "On failure settings switch to off.");
+ mSwitch.setChecked(false);
+ }
+ mToaster.toast("Unable to scan.");
+ }
+ };
+ }
+
+ private InputChannelCallback<Update> makeUpdateCallback() {
+ return new InputChannelCallback<Update>() {
+ @Override
+ public ListenableFuture<Void> onNext(Update result) {
+ mRemoteMoments.scanUpdateReceived(result);
+ return Futures.immediateFuture(null);
+ }
+ };
+ }
+
+ /** Verify that scanning is off and that the UX reflects that fact. */
+ private void cleanUpPostStop() {
+ Log.d(TAG, "cleanUpPostStop");
+ if (mScanner.isScanning()) {
+ throw new IllegalStateException("Scanning should be off.");
+ }
+ if (mToaster.isDestroyed()) {
+ Log.d(TAG, "The activity is dead, no UX to fix.");
+ return;
+ }
+ mRemoteMoments.dropAll();
+ if (mSwitch.isChecked()) {
+ // The button might be checked if the scan expired.
+ // The following call will trigger onCheckedChanged(false), but
+ // shouldn't do more than change the UX because everything is
+ // already shutdown.
+ mSwitch.setChecked(false);
+ }
+ }
+
+ private FutureCallback<Void> makeCompletionCallback() {
+ return new FutureCallback<Void>() {
+ /** Likely this is never called. */
+ @Override
+ public void onSuccess(Void result) {
+ cleanUpPostStop();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof CancellationException) {
+ // At the time of writing, the only way scanning ends
+ // is by throwing this exception, so this is actually
+ // a non-exceptional success case.
+ cleanUpPostStop();
+ } else {
+ Log.d(TAG, "Failure to gracefully stop scanning.", t);
+ }
+ }
+ };
+ }
+}
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java b/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java
index c97d9a4..485bdf7 100644
--- a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java
@@ -4,6 +4,8 @@
package io.v.moments.model;
+import com.google.common.util.concurrent.FutureCallback;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -61,16 +63,36 @@
ServerStatus mServerStatus;
@Mock
VContext mContext;
+ @Mock
+ FutureCallback<Void> mStartupCallback;
+ @Mock
+ FutureCallback<Void> mShutdownCallback;
+ @Mock
+ FutureCallback<Void> mStartupCallback2;
+ @Mock
+ FutureCallback<Void> mShutdownCallback2;
@Captor
ArgumentCaptor<Service> mAdvertisement;
@Captor
ArgumentCaptor<List<BlessingPattern>> mBlessingList;
+ @Captor
+ ArgumentCaptor<FutureCallback<VContext>> mV23StartupCallback;
+ @Captor
+ ArgumentCaptor<FutureCallback<Void>> mV23ShutdownCallback;
+ @Captor
+ ArgumentCaptor<Throwable> mThrowable;
Attributes mAttrs;
AdvertiserImpl mAdvertiser;
+ static Map<String, String> makeFakeAttributes() {
+ Map<String, String> result = new HashMap<>();
+ result.put("color", "teal");
+ return result;
+ }
+
@Before
public void setup() throws Exception {
mAttrs = new Attributes(makeFakeAttributes());
@@ -90,11 +112,6 @@
when(mMoment.toString()).thenReturn(MOMENT_NAME);
when(mMoment.makeAttributes()).thenReturn(mAttrs);
- List<BlessingPattern> list = any();
- when(mV23Manager.advertise(
- any(Service.class),
- list)).thenReturn(mContext);
-
mAdvertiser = new AdvertiserImpl(mV23Manager, mMoment);
}
@@ -115,57 +132,68 @@
@Test
public void advertiseStartSuccess() {
assertFalse(mAdvertiser.isAdvertising());
- mAdvertiser.advertiseStart();
+ mAdvertiser.start(mStartupCallback, mShutdownCallback);
- verify(mV23Manager).advertise(
- mAdvertisement.capture(),
- mBlessingList.capture());
-
- assertEquals(0, mBlessingList.getValue().size());
+ verifyAdvertiseCall();
Service service = mAdvertisement.getValue();
assertEquals(ID.toString(), service.getInstanceId());
assertEquals(MOMENT_NAME, service.getInstanceName());
- assertEquals(Config.INTERFACE_NAME, service.getInterfaceName());
+ assertEquals(Config.Discovery.INTERFACE_NAME, service.getInterfaceName());
assertSame(mAttrs, service.getAttrs());
List<String> addresses = service.getAddrs();
assertTrue(addresses.contains(ADDRESS));
assertEquals(1, addresses.size());
+ assertSame(mV23ShutdownCallback.getValue(), mShutdownCallback);
+
+ assertFalse(mAdvertiser.isAdvertising());
+ activateAdvertising();
assertTrue(mAdvertiser.isAdvertising());
}
+ private void verifyAdvertiseCall() {
+ verify(mV23Manager).advertise(
+ mAdvertisement.capture(),
+ eq(Config.Discovery.DURATION),
+ mV23StartupCallback.capture(),
+ mV23ShutdownCallback.capture());
+ }
+
+ // Make the transition to advertising by returning a context from V23.
+ private void activateAdvertising() {
+ mV23StartupCallback.getValue().onSuccess(mContext);
+ // Verify that the UX will be impacted.
+ verify(mStartupCallback).onSuccess(null);
+ }
+
@Test
public void advertiseStartFailure() {
assertFalse(mAdvertiser.isAdvertising());
- mAdvertiser.advertiseStart();
+ mAdvertiser.start(mStartupCallback, mShutdownCallback);
+ verifyAdvertiseCall();
+ activateAdvertising();
assertTrue(mAdvertiser.isAdvertising());
- mThrown.expect(IllegalStateException.class);
- mThrown.expectMessage("Already advertising.");
- mAdvertiser.advertiseStart();
+ mAdvertiser.start(mStartupCallback2, mShutdownCallback2);
+ verify(mStartupCallback2).onFailure(mThrowable.capture());
+ assertEquals("Already advertising.", mThrowable.getValue().getMessage());
}
@Test
- public void advertiseStopFailure() {
+ public void advertiseStopDoesNotThrowIfNotAdvertising() {
mThrown.expect(IllegalStateException.class);
mThrown.expectMessage("Not advertising.");
- mAdvertiser.advertiseStop();
+ mAdvertiser.stop();
}
@Test
public void advertiseStopSuccess() throws Exception {
advertiseStartSuccess();
assertTrue(mAdvertiser.isAdvertising());
- mAdvertiser.advertiseStop();
+ mAdvertiser.stop();
verify(mContext).cancel();
verify(mServerContext).cancel();
assertFalse(mAdvertiser.isAdvertising());
}
-
- static Map<String, String> makeFakeAttributes() {
- Map<String, String> result = new HashMap<>();
- result.put("color", "teal");
- return result;
- }
}