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(