TBR Java: updated wakeup support.
MultiPart: 2/2
Change-Id: I7f0bc7118374f1aa4ba820c164d0d24046aed350
diff --git a/android-lib/src/main/AndroidManifest.xml b/android-lib/src/main/AndroidManifest.xml
index e83fd05..f2d03e6 100644
--- a/android-lib/src/main/AndroidManifest.xml
+++ b/android-lib/src/main/AndroidManifest.xml
@@ -38,6 +38,9 @@
<service
android:name="io.v.android.impl.google.services.gcm.GcmReceiveListenerService"
android:exported="false" >
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ </intent-filter>
</service>
<service
android:name="io.v.android.impl.google.services.gcm.GcmRegistrationService"
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java
index 2b2844b..366ff85 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java
@@ -4,25 +4,182 @@
package io.v.android.impl.google.services.gcm;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.os.Bundle;
+import android.os.IBinder;
import android.util.Log;
import com.google.android.gms.gcm.GcmListenerService;
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.common.util.concurrent.Uninterruptibles;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+
+import javax.annotation.Nullable;
+
+import io.v.android.v23.V;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
import io.v.v23.verror.VException;
/**
* Listens for GCM messages and wakes up services upon their receipt.
*/
public class GcmReceiveListenerService extends GcmListenerService {
- private static final String TAG = "GcmRecvListenerService";
+ private static final String TAG = "GcmRecvListService";
+ private static final String GCE_PROJECT_ID = "632758215260";
+
+ private VContext mBaseContext;
+
+ @Override
+ public void onCreate() {
+ mBaseContext = V.init(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ mBaseContext.cancel();
+ }
@Override
public void onMessageReceived(String from, Bundle data) {
+ if (!from.equals(GCE_PROJECT_ID)) {
+ Log.e(TAG, "Unknown GCM sender: " + from);
+ return;
+ }
+ if (data == null) {
+ return;
+ }
+ boolean wakeup = data.containsKey("vanadium_wakeup");
+ if (!wakeup) {
+ return;
+ }
+ String messageId = data.getString("message_id");
+ if (messageId == null || messageId.isEmpty()) {
+ Log.e(TAG, "Empty message_id field.");
+ return;
+ }
+ String serviceName = data.getString("service_name");
+ if (serviceName == null || serviceName.isEmpty()) {
+ Log.e(TAG, "Empty service_name field.");
+ return;
+ }
+ if (!serviceName.startsWith(getPackageName())) {
+ return;
+ }
+ ComponentName service = new ComponentName(getPackageName(), serviceName);
+ if (!GcmRegistrationService.isServiceRegistered(this, service)) {
+ sendMsg(messageId, String.format("Service %s not registered for wakeup", serviceName));
+ return;
+ }
+ wakeupService(service, messageId);
+ }
+
+ private void wakeupService(final ComponentName service, String msgId) {
+ final SettableFuture<String> initDone = SettableFuture.create();
+ Intent intent = new Intent();
+ intent.setComponent(service);
+ ServiceConnection conn = new ServiceConnection() {
+ private boolean connected;
+
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder binder) {
+ connected = true;
+ Futures.addCallback(onServiceReady(service, binder), new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
+ initDone.set("");
+ }
+ @Override
+ public void onFailure(Throwable t) {
+ initDone.set(t.toString());
+ }
+ });
+ }
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ if (!connected) {
+ initDone.set("Couldn't connect to service " + componentName);
+ }
+ }
+ };
+ bindService(intent, conn, Context.BIND_AUTO_CREATE);
+ String error;
try {
- Util.wakeupServices(this, false);
- } catch (VException e) {
- Log.e(TAG, "Couldn't wakeup services.", e);
+ error = Uninterruptibles.getUninterruptibly(initDone);
+ } catch (ExecutionException e) {
+ error = e.toString();
+ }
+ // Start the service and unbind from it.
+ startService(intent);
+ unbindService(conn);
+ sendMsg(msgId, error);
+ }
+
+ private void sendMsg(String msgId, String error) {
+ Bundle b = new Bundle();
+ b.putString("error", error);
+ try {
+ GoogleCloudMessaging.getInstance(this).send(
+ GCE_PROJECT_ID + "@gcm.googleapis.com", msgId, 1, b);
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't send GCM message.");
}
}
-}
\ No newline at end of file
+
+ private ListenableFuture<Void> onServiceReady(ComponentName service, IBinder binder) {
+ if (binder == null) {
+ Log.d(TAG, "Service %s returned null binder.");
+ return Futures.immediateFuture(null);
+ }
+ Method getServiceMethod;
+ try {
+ getServiceMethod = binder.getClass().getDeclaredMethod("getServer");
+ } catch (NoSuchMethodException e) {
+ Log.d(TAG, String.format(
+ "Service %s binder doesn't have getServer() method: %s",
+ service, e.toString()));
+ return Futures.immediateFuture(null);
+ } catch (SecurityException e) {
+ Log.e(TAG, String.format(
+ "Don't have permissions to find method information for service %s binder: %s",
+ service, e.toString()));
+ return Futures.immediateFuture(null);
+ }
+ if (!getServiceMethod.getReturnType().isAssignableFrom(Server.class)) {
+ Log.e(TAG, String.format(
+ "Service %s binder's getServer() method doesn't return a " +
+ "Vanadium Service object.", service));
+ return Futures.immediateFuture(null);
+ }
+ Server server;
+ try {
+ server = (Server) getServiceMethod.invoke(binder);
+ } catch (Exception e) {
+ Log.e(TAG, String.format(
+ "Error invoking service %s binder's getService() method: %s",
+ service, e.toString()));
+ return Futures.immediateFuture(null);
+ }
+ if (server == null) {
+ Log.e(TAG, String.format(
+ "Service %s binder's getServer() method returned NULL Vanadium Server",
+ service));
+ return Futures.immediateFuture(null);
+ }
+ return server.allPublished(mBaseContext);
+ }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java
index 45936b7..590afba 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java
@@ -5,6 +5,8 @@
package io.v.android.impl.google.services.gcm;
import android.app.IntentService;
+import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
@@ -13,41 +15,246 @@
import com.google.android.gms.gcm.GoogleCloudMessaging;
import com.google.android.gms.iid.InstanceID;
-import java.io.IOException;
+import org.joda.time.Duration;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+import io.v.android.v23.V;
+import io.v.v23.VFutures;
+import io.v.v23.context.VContext;
+import io.v.v23.services.wakeup.WakeUpClient;
+import io.v.v23.services.wakeup.WakeUpClientFactory;
import io.v.v23.verror.VException;
/**
- * Communicates with GCM servers to obtain a new registration token and then starts
- * all app services that have registered themselves as wake-able.
+ * Communicates with GCM servers to obtain a new registration token and then controls
+ * all app services that have registered themselves as persistent.
*/
public class GcmRegistrationService extends IntentService {
+ /**
+ * A key in {@link SharedPreferences} under which the mount root for
+ * app's persistent services is stored.
+ */
+ public static final String WAKEUP_MOUNT_ROOT_PREF_KEY =
+ "io.v.android.impl.google.services.gcm.WAKEUP_MOUNT_ROOT";
+
+ /**
+ * Registers the service and starts it (via {@link #startService}).
+ * <p>
+ * If the registration fails, the service will be started but may never be woken up
+ * afterward.
+ * <p>
+ * If the service has already been registered it will only be started.
+ */
+ public static void registerAndStartService(Context ctx, ComponentName service) {
+ Intent intent = new Intent(ctx, GcmRegistrationService.class);
+ intent.putExtra(EXTRA_MODE, Mode.REGISTER_AND_START_SERVICE.getValue());
+ intent.putExtra(EXTRA_SERVICE_INFO, service.flattenToString());
+ ctx.startService(intent);
+ }
+
+ /**
+ * Unregisters the services and stops it (via {@link #stopService}).
+ */
+ public static void unregisterAndStopService(Context ctx, ComponentName service) {
+ Intent intent = new Intent(ctx, GcmRegistrationService.class);
+ intent.putExtra(EXTRA_MODE, Mode.UNREGISTER_AND_STOP_SERVICE.getValue());
+ intent.putExtra(EXTRA_SERVICE_INFO, service.flattenToString());
+ ctx.startService(intent);
+ }
+
+ /**
+ * Refreshes the GCM token and restarts all registered services (via {@link #stopService}
+ * followed by {@link #startService}.
+ */
+ public static void refreshTokenAndRestartRegisteredServices(Context ctx) {
+ Intent intent = new Intent(ctx, GcmRegistrationService.class);
+ intent.putExtra(EXTRA_MODE, Mode.REFRESH_TOKEN_AND_RESTART_REGISTERED_SERVICES.getValue());
+ ctx.startService(intent);
+ }
+
+ // Stores the set of all persistent services' flattened ComponentNames.
+ private static final String REGISTERED_SERVICES_PREF_KEY =
+ "io.v.android.impl.google.services.gcm.REGISTERED_SERVICES_PREF_KEY";
+ private static final String EXTRA_MODE = "EXTRA_MODE";
+ private static final String EXTRA_SERVICE_INFO= "EXTRA_SERVICE_INFO";
+ private static final String VANADIUM_WAKEUP_SERVICE =
+ "/ns.dev.v.io:8101/users/jenkins.veyron@gmail.com/wakeup/server";
private static final String TAG = "GcmRegistrationService";
- public static final String GCM_TOKEN_PREF_KEY = "io.v.android.impl.google.services.gcm.TOKEN";
- static final String EXTRA_RESTART_SERVICES = "RESTART_SERVICES";
+
+ private enum Mode {
+ REGISTER_AND_START_SERVICE (1),
+ UNREGISTER_AND_STOP_SERVICE (2),
+ REFRESH_TOKEN_AND_RESTART_REGISTERED_SERVICES (3);
+
+ private final int value;
+ Mode(int value) {
+ this.value = value;
+ }
+
+ int getValue() {
+ return value;
+ }
+
+ static Mode fromInt(int value) {
+ for (Mode m : Mode.values()) {
+ if (m.getValue() == value) {
+ return m;
+ }
+ }
+ return null;
+ }
+ }
public GcmRegistrationService() {
super("GcmRegistrationService");
}
@Override
- protected void onHandleIntent(Intent intent) {
+ protected synchronized void onHandleIntent(Intent intent) {
+ Mode mode = Mode.fromInt(intent.getIntExtra(EXTRA_MODE, -1));
+ switch (mode) {
+ case REGISTER_AND_START_SERVICE:
+ registerAndStartService(intent);
+ break;
+ case UNREGISTER_AND_STOP_SERVICE:
+ unregisterAndStopService(intent);
+ break;
+ case REFRESH_TOKEN_AND_RESTART_REGISTERED_SERVICES:
+ refreshTokenAndRestartRegisteredServices();
+ break;
+ default:
+ Log.e(TAG, String.format("Invalid mode %s for GCMRegistrationService. " +
+ "Dropping the request.", mode));
+ }
+ }
+
+ private void registerAndStartService(Intent intent) {
+ ComponentName service = ComponentName.unflattenFromString(
+ intent.getStringExtra(EXTRA_SERVICE_INFO));
+ if (service == null) {
+ Log.e(TAG, "Couldn't extract service information from intent - dropping the request.");
+ return;
+ }
+ registerService(service);
+ if (loadMountRoot().isEmpty()) {
+ refreshToken();
+ }
+ startService(service);
+ }
+
+ private void unregisterAndStopService(Intent intent) {
+ ComponentName service = intent.getParcelableExtra(EXTRA_SERVICE_INFO);
+ if (service == null) {
+ Log.e(TAG, "Couldn't extract service information from intent - dropping the request.");
+ return;
+ }
+ unregisterService(service);
+ stopService(service);
+ }
+
+ private void refreshTokenAndRestartRegisteredServices() {
+ refreshToken();
+ for (ComponentName service : getRegisteredServices(this)) {
+ stopService(service);
+ startService(service);
+ }
+ }
+
+ private void refreshToken() {
+ VContext ctx = V.init(this);
try {
+ // Get token from Google GCM servers.
InstanceID instanceID = InstanceID.getInstance(this);
- String token = instanceID.getToken("*", GoogleCloudMessaging.INSTANCE_ID_SCOPE);
- // Store registration token in SharedPreferences.
- SharedPreferences.Editor editor =
- PreferenceManager.getDefaultSharedPreferences(this).edit();
- editor.putString(GCM_TOKEN_PREF_KEY, token);
- editor.commit();
+ // TODO(spetrovic): Add a TTL for the token.
+ String token = instanceID.getToken("632758215260", GoogleCloudMessaging.INSTANCE_ID_SCOPE);
+
+ Log.d(TAG, "Token is: " + token);
+
+ // Register the token with the Vanadium wakeup service.
+ WakeUpClient wake = WakeUpClientFactory.getWakeUpClient(VANADIUM_WAKEUP_SERVICE);
+ VContext ctxT = ctx.withTimeout(Duration.standardSeconds(20));
+ String mountRoot = VFutures.sync(wake.register(ctxT, token));
+
+ // Store the wakeup mount root in SharedPreferences.
+ storeMountRoot(mountRoot);
} catch (IOException e) {
- Log.e(TAG, "Couldn't fetch GCM registration token: ", e);
- }
- boolean restartServices = intent.getBooleanExtra(EXTRA_RESTART_SERVICES, false);
- try {
- Util.wakeupServices(this, restartServices);
+ Log.e(TAG, "Couldn't fetch GCM registration token: " + e.toString());
} catch (VException e) {
- Log.e(TAG, "Couldn't wakeup services.", e);
+ Log.e(TAG, "Couldn't register GCM token with vanadium services: " + e.toString());
+ } finally {
+ ctx.cancel();
}
}
+
+ private void registerService(ComponentName service) {
+ Set<String> s = loadRegisteredServices(this);
+ s.add(service.flattenToString());
+ storeRegisteredServices(s);
+ }
+
+ private void unregisterService(ComponentName service) {
+ Set<String> s = loadRegisteredServices(this);
+ s.remove(service.flattenToString());
+ storeRegisteredServices(s);
+ }
+
+ /**
+ * Returns {@code true} iff the given {@code service} is a registered persistent service.
+ */
+ public static boolean isServiceRegistered(Context context, ComponentName service) {
+ Set<String> s = loadRegisteredServices(context);
+ return s.contains(service.flattenToString());
+ }
+
+ /**
+ * Returns a list of all registered persistent services.
+ */
+ public static ComponentName[] getRegisteredServices(Context context) {
+ Set<String> s = loadRegisteredServices(context);
+ String[] names = s.toArray(new String[s.size()]);
+ ComponentName[] ret = new ComponentName[s.size()];
+ for (int i = 0; i < names.length; ++i) {
+ ret[i] = ComponentName.unflattenFromString(names[i]);
+ }
+ return ret;
+ }
+
+ private static Set<String> loadRegisteredServices(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getStringSet(
+ REGISTERED_SERVICES_PREF_KEY, new HashSet<String>());
+ }
+
+ private void storeRegisteredServices(Set<String> services) {
+ SharedPreferences.Editor editor =
+ PreferenceManager.getDefaultSharedPreferences(this).edit();
+ editor.putStringSet(REGISTERED_SERVICES_PREF_KEY, services);
+ editor.commit();
+ }
+
+ private String loadMountRoot() {
+ return PreferenceManager.getDefaultSharedPreferences(this).getString(
+ WAKEUP_MOUNT_ROOT_PREF_KEY, "");
+ }
+
+ private void storeMountRoot(String mountRoot) {
+ SharedPreferences.Editor editor =
+ PreferenceManager.getDefaultSharedPreferences(this).edit();
+ editor.putString(WAKEUP_MOUNT_ROOT_PREF_KEY, mountRoot);
+ editor.commit();
+ }
+
+ private void startService(ComponentName service) {
+ final Intent intent = new Intent();
+ intent.setComponent(service);
+ startService(intent);
+ }
+
+ private void stopService(ComponentName service) {
+ Intent intent = new Intent();
+ intent.setComponent(service);
+ stopService(intent);
+ }
}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java
index a95eb5f..d21a60d 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java
@@ -4,8 +4,6 @@
package io.v.android.impl.google.services.gcm;
-import android.content.Intent;
-
import com.google.android.gms.iid.InstanceIDListenerService;
/**
@@ -14,8 +12,6 @@
public class GcmTokenRefreshListenerService extends InstanceIDListenerService {
@Override
public void onTokenRefresh() {
- Intent intent = new Intent(this, GcmRegistrationService.class);
- intent.putExtra(GcmRegistrationService.EXTRA_RESTART_SERVICES, true);
- startService(intent);
+ GcmRegistrationService.refreshTokenAndRestartRegisteredServices(this);
}
}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java
index a0ecc23..3b64938 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java
@@ -7,83 +7,25 @@
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ServiceInfo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import io.v.v23.verror.VException;
/**
* Utility GCM methods.
*/
-class Util {
- // Wakes up all services that have marked themselves for wakeup.
- // If restart==true, services are first stopped and then started; otherwise, they
- // are just started.
- static void wakeupServices(Context context, boolean restart) throws VException {
- for (ServiceInfo service : getWakeableServices(context)) {
- Intent intent = new Intent();
- intent.setComponent(new ComponentName(service.packageName, service.name));
- if (restart) {
- context.stopService(intent);
- }
- context.startService(intent);
- }
- }
-
- // Returns a list of all services that have marked themselves for wakeup.
- static List<ServiceInfo> getWakeableServices(Context context) throws VException {
- try {
- ServiceInfo[] services = context.getPackageManager().getPackageInfo(
- context.getPackageName(),
- PackageManager.GET_META_DATA|PackageManager.GET_SERVICES).services;
- if (services == null) {
- throw new VException("Couldn't get services information for package: " +
- context.getPackageName());
- }
- ArrayList<ServiceInfo> ret = new ArrayList<>();
- for (ServiceInfo service : services) {
- if (service == null) continue;
- if (!service.packageName.equals(context.getPackageName())) continue;
- if (service.metaData == null) continue;
- boolean wakeup = service.metaData.getBoolean("wakeup", false);
- if (!wakeup) continue;
- ret.add(service);
- }
- return ret;
- } catch (PackageManager.NameNotFoundException e) {
- throw new VException(String.format(
- "Couldn't get package information for package %s: %s",
- context.getPackageName(), e.getMessage()));
- }
- }
-
+public class Util {
/**
- * Returns {@code true} iff the provided service is "wakeable", i.e., if it has the
- * {@code "wakeup"} metadata attached to it.
+ * Returns {@code true} iff the provided service is <em>persistent</em>, i.e., if it
+ * can respond to Vanadium RPC requests even when the starting activity has been destroyed.
*
* @param context Android context representing the service
- * @return {@code true} iff the provided service is "wakeable"
- * @throws VException if there was an error figuring out if a service is wakeable
+ * @return {@code true} iff the provided service is <em>persistent</em>
*/
- public static boolean isServiceWakeable(Context context) throws VException {
+ public static boolean isServicePersistent(Context context) {
if (!(context instanceof Service)) {
return false;
}
Service service = (Service) context;
- try {
- ServiceInfo info = service.getPackageManager().getServiceInfo(
- new ComponentName(service, service.getClass()), PackageManager.GET_META_DATA);
- if (info.metaData == null) {
- return false;
- }
- return info.metaData.getBoolean("wakeup", false);
- } catch (PackageManager.NameNotFoundException e) {
- throw new VException(e.getMessage());
- }
+ return GcmRegistrationService.isServiceRegistered(context,
+ new ComponentName(service, service.getClass()));
}
private Util() {}
diff --git a/android-lib/src/main/java/io/v/android/v23/V.java b/android-lib/src/main/java/io/v/android/v23/V.java
index 2b7cb2a..cbd78c6 100644
--- a/android-lib/src/main/java/io/v/android/v23/V.java
+++ b/android-lib/src/main/java/io/v/android/v23/V.java
@@ -4,12 +4,16 @@
package io.v.android.v23;
+import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
+import android.preference.PreferenceManager;
+import android.util.Log;
import com.google.common.base.Preconditions;
import io.v.android.impl.google.services.gcm.GcmRegistrationService;
+import io.v.android.impl.google.services.gcm.Util;
+import io.v.impl.google.naming.NamingUtil;
import io.v.v23.Options;
import io.v.v23.context.VContext;
import io.v.v23.security.Blessings;
@@ -43,15 +47,15 @@
* </pre></blockquote><p>
*/
public class V extends io.v.v23.V {
+ private static final String TAG = "Vanadium";
private static native void nativeInitGlobalAndroid(Options opts) throws VException;
private static volatile VContext globalContext;
// Initializes the Vanadium Android-specific global state.
- private static VContext initGlobalAndroid(VContext ctx, Options opts) {
+ private static void initGlobalAndroid(Options opts) {
try {
nativeInitGlobalAndroid(opts);
- return V.withExecutor(ctx, UiThreadExecutor.INSTANCE);
} catch (VException e) {
throw new RuntimeException("Couldn't initialize global Android state", e);
}
@@ -68,8 +72,12 @@
return globalContext;
}
if (opts == null) opts = new Options();
- VContext ctx = initGlobalShared(opts);
- ctx = initGlobalAndroid(ctx, opts);
+ initGlobalShared();
+ initGlobalAndroid(opts); // MUST be called before initRuntime()
+ VContext ctx = initRuntime(opts);
+ // Set the default executor to be the UT thread executor.
+ ctx = V.withExecutor(ctx, UiThreadExecutor.INSTANCE);
+
// Set the VException component name to the Android context package name.
ctx = VException.contextWithComponentName(ctx, androidCtx.getPackageName());
try {
@@ -79,20 +87,26 @@
throw new RuntimeException("Couldn't setup Vanadium principal", e);
}
globalContext = ctx;
- // Start the GCM registration service, which obtains the GCM token for the app
- // (if the app is configured to use GCM).
- // NOTE: this call may lead to a recursive call to V.init() (in a separate thread),
- // so keep this code below the line where 'globalContext' is set, or we may run
- // into an infinite recursion.
- Intent intent = new Intent(androidCtx, GcmRegistrationService.class);
- androidCtx.startService(intent);
return ctx;
}
}
// Initializes Vanadium state that's local to the invoking activity/service.
private static VContext initAndroidLocal(VContext ctx, Context androidCtx, Options opts) {
- return ctx.withValue(new AndroidContextKey(), androidCtx);
+ ctx = ctx.withValue(new AndroidContextKey(), androidCtx);
+ if (Util.isServicePersistent(androidCtx)) {
+ // Set the new namespace, which will trigger the creation of a namespace that
+ // understands how to wakeup the service.
+ try {
+ ctx = V.withNewNamespace(
+ ctx, V.getNamespace(ctx).getRoots().toArray(new String[]{}));
+ } catch (VException e) {
+ throw new RuntimeException(String.format(
+ "Couldn't set namespace for persistent service %s: %s",
+ androidCtx.getClass().getName(), e.toString()));
+ }
+ }
+ return ctx;
}
/**
@@ -113,7 +127,7 @@
* <p>
* If this option isn't provided, the default runtime implementation is used.
*
- * @param androidCtx Android application context
+ * @param androidCtx Android context
* @param opts options for the default runtime
* @return base context
*/
@@ -137,6 +151,28 @@
throw new RuntimeException("Must call Android init with a context.");
}
+ /**
+ * Starts a <em>permanent</em> service, i.e., a service that can respond to Vanadium RPC
+ * requests even when it has been destroyed.
+ *
+ * @param androidCtx Android context
+ * @param service the service to start
+ */
+ public static void startPermanentService(Context androidCtx, ComponentName service) {
+ GcmRegistrationService.registerAndStartService(androidCtx, service);
+ }
+
+ /**
+ * Stops a <em>permanent</em> service, i.e., a service that can respond to Vanadium RPC
+ * requests even when it has been destroyed.
+ *
+ * @param androidCtx Android context
+ * @param service the service to stop
+ */
+ public static void stopPermanentService(Context androidCtx, ComponentName service) {
+ GcmRegistrationService.unregisterAndStopService(androidCtx, service);
+ }
+
private static VPrincipal createPrincipal(Context ctx) throws VException {
// Check if the private key has already been generated for this package.
// (NOTE: Android package names are unique.)
@@ -181,5 +217,21 @@
}
}
+ // Invoked from JNI code.
+ private static String getWakeupMountRoot(VContext ctx) {
+ Context androidCtx = getAndroidContext(ctx);
+ if (androidCtx == null || !Util.isServicePersistent(androidCtx)) {
+ // No wakeup.
+ return "";
+ }
+ String mtRoot = PreferenceManager.getDefaultSharedPreferences(androidCtx).getString(
+ GcmRegistrationService.WAKEUP_MOUNT_ROOT_PREF_KEY, "");
+ if (mtRoot.isEmpty()) {
+ Log.e(TAG, "Empty wakeup mount root.");
+ return "";
+ }
+ return NamingUtil.join(mtRoot, androidCtx.getClass().getName());
+ }
+
private V() {}
}
diff --git a/lib/src/main/java/io/v/impl/google/namespace/NamespaceImpl.java b/lib/src/main/java/io/v/impl/google/namespace/NamespaceImpl.java
index 05016eb..0d42413 100644
--- a/lib/src/main/java/io/v/impl/google/namespace/NamespaceImpl.java
+++ b/lib/src/main/java/io/v/impl/google/namespace/NamespaceImpl.java
@@ -45,9 +45,11 @@
Callback<MountEntry> callback);
private static native void nativeResolve(long nativeRef, VContext context, String name,
Options options, Callback<MountEntry> callback);
+ private static native boolean nativeSetCachingPolicy(long nativeRef, boolean doCaching);
private static native boolean nativeFlushCacheEntry(long nativeRef, VContext context,
String name);
private static native void nativeSetRoots(long nativeRef, List<String> roots) throws VException;
+ private static native List<String> nativeGetRoots(long nativeRef) throws VException;
private static native void nativeSetPermissions(long nativeRef, VContext context, String name,
Permissions permissions, String version,
Options options, Callback<Void> callback);
@@ -126,6 +128,11 @@
}
@Override
+ public boolean setCachingPolicy(boolean doCaching) {
+ return nativeSetCachingPolicy(nativeRef, doCaching);
+ }
+
+ @Override
public boolean flushCacheEntry(VContext ctx, String name) {
return nativeFlushCacheEntry(nativeRef, ctx, name);
}
@@ -150,6 +157,15 @@
}
@Override
+ public List<String> getRoots() {
+ try {
+ return nativeGetRoots(nativeRef);
+ } catch (VException e) {
+ throw new RuntimeException("Couldn't get roots.", e);
+ }
+ }
+
+ @Override
public ListenableFuture<Void> setPermissions(VContext ctx, String name,
Permissions permissions, String version) {
return setPermissions(ctx, name, permissions, version, null);
@@ -189,16 +205,16 @@
if (this.getClass() != other.getClass()) {
return false;
}
- return this.nativeRef == ((NamespaceImpl) other).nativeRef;
+ return nativeRef == ((NamespaceImpl) other).nativeRef;
}
@Override
public int hashCode() {
- return Long.valueOf(this.nativeRef).hashCode();
+ return Long.valueOf(nativeRef).hashCode();
}
@Override
protected void finalize() {
- nativeFinalize(this.nativeRef);
+ nativeFinalize(nativeRef);
}
}
diff --git a/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java b/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
index 303165f..f3236ca 100644
--- a/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
+++ b/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
@@ -4,6 +4,11 @@
package io.v.impl.google.rpc;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import io.v.impl.google.ListenableFutureCallback;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Callback;
import io.v.v23.rpc.Server;
import io.v.v23.rpc.ServerStatus;
import io.v.v23.verror.VException;
@@ -14,6 +19,7 @@
private native void nativeAddName(long nativeRef, String name) throws VException;
private native void nativeRemoveName(long nativeRef, String name);
private native ServerStatus nativeGetStatus(long nativeRef) throws VException;
+ private native void nativeAllPublished(long nativeRef, VContext ctx, Callback<Void> callback);
private native void nativeFinalize(long nativeRef);
private ServerImpl(long nativeRef) {
@@ -36,6 +42,12 @@
throw new RuntimeException("Couldn't get status", e);
}
}
+ @Override
+ public ListenableFuture<Void> allPublished(VContext ctx) {
+ ListenableFutureCallback<Void> callback = new ListenableFutureCallback<>();
+ nativeAllPublished(nativeRef, ctx, callback);
+ return callback.getFuture(ctx);
+ }
// Implement java.lang.Object.
@Override
public boolean equals(Object other) {
diff --git a/lib/src/main/java/io/v/impl/google/rt/VRuntimeImpl.java b/lib/src/main/java/io/v/impl/google/rt/VRuntimeImpl.java
index eaf229f..1ca07bc 100644
--- a/lib/src/main/java/io/v/impl/google/rt/VRuntimeImpl.java
+++ b/lib/src/main/java/io/v/impl/google/rt/VRuntimeImpl.java
@@ -38,8 +38,7 @@
*/
public class VRuntimeImpl implements VRuntime {
private static native VContext nativeInit() throws VException;
- private static native ListenableFuture<Void> nativeShutdown(VContext context,
- Callback<Void> callback);
+ private static native void nativeShutdown(VContext context, Callback<Void> callback);
private static native VContext nativeWithNewClient(VContext ctx, Options opts)
throws VException;
private static native Client nativeGetClient(VContext ctx) throws VException;
diff --git a/lib/src/main/java/io/v/v23/V.java b/lib/src/main/java/io/v/v23/V.java
index 2f5d40a..2e45447 100644
--- a/lib/src/main/java/io/v/v23/V.java
+++ b/lib/src/main/java/io/v/v23/V.java
@@ -64,7 +64,7 @@
// Initializes the Vanadium global state, i.e., state that needs to be cleaned up only
// before the process is about to terminate. This method is shared between Java and Android
// implementations.
- protected static VContext initGlobalShared(Options opts) {
+ protected static void initGlobalShared() {
List<Throwable> errors = new ArrayList<Throwable>();
try {
// First, attempt to find the library in java.library.path.
@@ -119,6 +119,11 @@
} catch (VException e) {
throw new RuntimeException("Couldn't register caveat validators", e);
}
+ }
+
+ // Initializes the Vanadium global runtime. This method is shared between Java and Android
+ // implementations.
+ protected static VContext initRuntime(Options opts) {
// See if a runtime was provided as an option.
if (opts.get(OptionDefs.RUNTIME) != null) {
runtime = opts.get(OptionDefs.RUNTIME, VRuntime.class);
@@ -158,7 +163,8 @@
return globalContext;
}
if (opts == null) opts = new Options();
- VContext ctx = initGlobalShared(opts);
+ initGlobalShared();
+ VContext ctx = initRuntime(opts);
ctx = initGlobalJava(ctx, opts);
// Set the VException component name to this binary name.
ctx = VException.contextWithComponentName(ctx, System.getProperty("program.name", ""));
diff --git a/lib/src/main/java/io/v/v23/namespace/Namespace.java b/lib/src/main/java/io/v/v23/namespace/Namespace.java
index 2513516..b80e7af 100644
--- a/lib/src/main/java/io/v/v23/namespace/Namespace.java
+++ b/lib/src/main/java/io/v/v23/namespace/Namespace.java
@@ -199,13 +199,23 @@
Options options);
/**
+ * Sets the new caching policy, returning the previous one.
+ * <p>
+ * This is a non-blocking method.
+ *
+ * @param doCaching if {@code true}, this namespace will cache entries; otherwise, it won't
+ */
+ boolean setCachingPolicy(boolean doCaching);
+
+ /**
* Flushes resolution information cached for the given name. If anything was flushed it returns
* {@code true}.
* <p>
* This is a non-blocking method.
*
* @param context a client context
- * @param name a Vanadium name, see also <a href="https://vanadium.github.io/glossary.html#object-name">the
+ * @param name a Vanadium name, see also
+ * <a href="https://vanadium.github.io/glossary.html#object-name">the
* Name entry</a> in the glossary
* @return {@code true} iff resolution information for the name was successfully flushed
*/
@@ -257,6 +267,15 @@
void setRoots(List<String> roots) throws VException;
/**
+ * Returns the currently configured roots.
+ * <p>
+ * An empty list is returned if no roots are configured.
+ * <p>
+ * This is a non-blocking method.
+ */
+ List<String> getRoots();
+
+ /**
* A shortcut for {@link #setPermissions(VContext, String, Permissions, String, Options)} with a
* {@code null} options parameter.
*/
@@ -289,6 +308,9 @@
* a {@link VException} is thrown indicating that this call had no effect. If the
* version number is not specified, no version check is performed
* @param options options to pass to the implementation as described above, or {@code null}
+ * @return a new {@link ListenableFuture} that completes when the permissions have
+ * been set
+ *
*/
@CheckReturnValue
ListenableFuture<Void> setPermissions(VContext context, String name, Permissions permissions,
@@ -320,9 +342,10 @@
* {@code context} gets canceled.
*
* @param context a client context
- * @param name the name of the node
+ * @param name the name of the node
* @param options options to pass to the implementation as described above, or {@code null}
- * @return a single-entry map from permissions version to permissions for the named object
+ * @return a new {@link ListenableFuture} whose result is a single-entry map from
+ * permissions version to permissions for the named object
*/
@CheckReturnValue
ListenableFuture<Map<String, Permissions>> getPermissions(VContext context, String name,
diff --git a/lib/src/main/java/io/v/v23/naming/OptionDefs.java b/lib/src/main/java/io/v/v23/naming/OptionDefs.java
new file mode 100644
index 0000000..ed201cc
--- /dev/null
+++ b/lib/src/main/java/io/v/v23/naming/OptionDefs.java
@@ -0,0 +1,27 @@
+// 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.v23.naming;
+
+/**
+ * Various options in the Vanadium naming package.
+ */
+public class OptionDefs {
+ /**
+ * A key for an option of type {@link Boolean} that specifies whether the mount should
+ * replace the previous mount.
+ */
+ public static final String REPLACE_MOUNT = "io.v.v23.naming.REPLACE_MOUNT";
+
+ /**
+ * A key for an option of type {@link Boolean} that specifies whether the target is a
+ * mount table.
+ */
+ public static final String SERVES_MOUNT_TABLE = "io.v.v23.naming.SERVES_MOUNT_TABLE";
+
+ /**
+ * A key for an option of type {@link Boolean} that specifies whether the target is a leaf.
+ */
+ public static final String IS_LEAF = "io.v.v23.naming.IS_LEAF";
+}
diff --git a/lib/src/main/java/io/v/v23/rpc/Server.java b/lib/src/main/java/io/v/v23/rpc/Server.java
index 4f9a69d..1f41fbc 100644
--- a/lib/src/main/java/io/v/v23/rpc/Server.java
+++ b/lib/src/main/java/io/v/v23/rpc/Server.java
@@ -4,6 +4,9 @@
package io.v.v23.rpc;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import io.v.v23.context.VContext;
import io.v.v23.verror.VException;
/**
@@ -27,6 +30,23 @@
void removeName(String name);
/**
+ * Returns a new {@link ListenableFuture} that completes when the server has successfully
+ * published all of its endpoints.
+ * <p>
+ * The returned future is guaranteed to be executed on an {@link java.util.concurrent.Executor}
+ * specified in {@code context} (see {@link io.v.v23.V#withExecutor}).
+ * <p>
+ * The returned future will fail with {@link java.util.concurrent.CancellationException} if
+ * {@code context} gets canceled.
+ *
+ * @param context a client context
+ * @return a new listenable future that completes when the server has successfully
+ * published all of its endpoints
+ *
+ */
+ ListenableFuture<Void> allPublished(VContext context);
+
+ /**
* Returns the current {@link ServerStatus} of the server.
*/
ServerStatus getStatus();
diff --git a/lib/src/main/java/io/v/v23/rpc/ServerStatus.java b/lib/src/main/java/io/v/v23/rpc/ServerStatus.java
index 15c066e..5e2ce8b 100644
--- a/lib/src/main/java/io/v/v23/rpc/ServerStatus.java
+++ b/lib/src/main/java/io/v/v23/rpc/ServerStatus.java
@@ -13,7 +13,6 @@
import java.util.Map;
import java.util.HashMap;
-
/**
* The current status of the server.
*/
@@ -37,7 +36,8 @@
* @param proxyErrors set of errors currently encountered from listening on proxies
*/
public ServerStatus(ServerState state, boolean servesMountTable, PublisherEntry[] entries,
- String[] endpoints, Map<Address, VException> lnErrors, Map<String, VException> proxyErrors) {
+ String[] endpoints, Map<Address, VException> lnErrors,
+ Map<String, VException> proxyErrors) {
this.state = state;
this.servesMountTable = servesMountTable;
this.entries = entries == null ? new PublisherEntry[0] : Arrays.copyOf(entries, entries.length);
@@ -90,8 +90,8 @@
}
/**
- * Returns the map of errors encountered when listening on the network. The returned
- * map is keyed by {@link Address addresses} in the ListenSpec.
+ * Returns the map of errors encountered when listening on proxies. The returned
+ * map is keyed by the name of the proxy specified in the {@link ListenSpec}.
*/
public Map<String,VException> getProxyErrors() {
return new HashMap<>(proxyErrors);
diff --git a/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java b/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
index 669b70e..e8dcf18 100644
--- a/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
+++ b/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
@@ -11,9 +11,11 @@
import com.google.common.util.concurrent.SettableFuture;
import com.google.common.util.concurrent.Uninterruptibles;
+import io.v.impl.google.namespace.NamespaceTestUtil;
import io.v.v23.InputChannels;
import io.v.v23.V;
import io.v.v23.V23TestUtil;
+import io.v.v23.VFutures;
import io.v.v23.context.VContext;
import io.v.v23.naming.GlobReply;
import io.v.v23.rpc.Client;
@@ -65,6 +67,7 @@
ctx = V.init();
ListenSpec.Address addr = new ListenSpec.Address("tcp", "127.0.0.1:0");
ctx = V.withListenSpec(ctx, V.getListenSpec(ctx).withAddress(addr));
+ ctx = NamespaceTestUtil.withTestMountServer(ctx);
}
@Override
@@ -209,6 +212,7 @@
public void onSuccess(String fortune) {
future.set(fortune);
}
+
@Override
public void onFailure(Throwable t) {
future.setException(t);
@@ -443,6 +447,20 @@
// that actually is populated with errors.
}
+ public void testAllPublishedNoName() throws Exception {
+ FortuneServer server = new FortuneServerImpl();
+ ctx = V.withNewServer(ctx, "", server, null);
+ Server s = V.getServer(ctx);
+ VFutures.sync(s.allPublished(ctx), 2, TimeUnit.SECONDS);
+ }
+
+ public void testAllPublished() throws Exception {
+ FortuneServer server = new FortuneServerImpl();
+ ctx = V.withNewServer(ctx, "test", server, null);
+ Server s = V.getServer(ctx);
+ VFutures.sync(s.allPublished(ctx), 2, TimeUnit.SECONDS);
+ }
+
private static class TestInvoker implements Invoker {
@Override
public ListenableFuture<Object[]> invoke(