Advanced topics

These sections are meant for reference and it is not required that you read them top-to-bottom.

Use framework APIs:

These APIs will be wrapped in the SDK for a more consistent API surface (e.g. avoiding UserHandle objects), but for now, you can call these directly.

The implementation is straightforward: if you can interact, go ahead. If not, but you can request, then show your user prompt/banner/tooltip/etc. If the user agrees to go to Settings, create the request intent and use Context#startActivity to send the user there. You can either use the broadcast to detect when this ability changes, or just check again when the user comes back.

To test this, you'll need to open TestDPC in your work profile, go to the very bottom and select to add your package name to the connected apps allowlist. This mimics the admin 'allow-listing' your app.

Glossary

This section defines key terms related to developing cross-profile development.

Cross Profile Configuration

A Cross Profile Configuration groups together related Cross Profile Provider Classes and provides general configuration for the cross-profile features. Typically there will be one @CrossProfileConfiguration annotation per codebase , but in some complex applications there may be multiple.

Profile Connector

A Connector manages connections between profiles. Typically each cross profile type will point to a specific Connector. Every cross profile type in a single configuration must use the same Connector.

Cross Profile Provider Class

A Cross Profile Provider Class groups together related Cross Profile Types.

Mediator

A mediator sits between high-level and low-level code, distributing calls to the correct profiles and merging results. This is the only code which needs to be profile-aware. This is an architectural concept rather than something built into the SDK.

Cross Profile Type

A cross profile type is a class or interface containing methods annotated @CrossProfile. The code in this type needs not be profile-aware and should ideally just act on its local data.

Profile Types

Profile Type
CurrentThe active profile that we are executing on.
Other(if it exists) The profile we are not executing on.
PersonalUser 0, the profile on the device that cannot be switched off.
WorkTypically user 10 but may be higher, can be toggled on and off, used to contain work apps and data.
PrimaryOptionally defined by the application. The profile which shows a merged view of both profiles.
SecondaryIf primary is defined, secondary is the profile which is not primary.
SupplierThe suppliers to the primary profile is both profiles, the suppliers to the secondary profile is only the secondary profile itself.

Profile Identifier

A class which represents a type of profile (personal or work). These will be returned by methods which run on multiple profiles and can be used to run more code on those profiles. These can be serialised to an int for convenient storage.

This guide outlines recommended structures for building efficient and maintainable cross-profile functionalities within your Android app.

Convert your CrossProfileConnector into a singleton

Only a single instance should be used throughout the lifecycle of your application, or else you will create parallel connections. This can be done either using a dependency injection framework such as Dagger, or by using a classic Singleton pattern, either in a new class or an existing one.

Inject or pass in the generated Profile instance into your class for when you make the call, rather than creating it in the method

This lets you to pass in the automatically-generated FakeProfile instance in your unit tests later.

Consider the mediator pattern

This common pattern is to make one of your existing APIs (e.g. getEvents()) profile-aware for all of its callers. In this case, your existing API can just become a 'mediator' method or class that contains the new call to generated cross-profile code.

This way, you don't force every caller to know to make a cross-profile call it just becomes part of your API.

Consider whether to annotate an interface method as @CrossProfile instead to avoid having to expose your implementation classes in a provider

This works nicely with dependency injection frameworks.

If you are receiving any data from a cross-profile call, consider whether to add a field referencing which profile it came from

This can be good practice since you might want to know this at the UI layer (e.g. adding a badge icon to work stuff). It also might be required if any data identifiers are no longer unique without it, such as package names.

Cross Profile

This section outlines how to build your own Cross Profile interactions.

Primary Profiles

Most of the calls in examples on this document contain explicit instructions on which profiles to run on, including work, personal, and both.

In practice, for apps with a merged experience on only one profile, you likely want this decision to depend on the profile that you are running on, so there are similar convenient methods that also take this into account, to avoid your codebase being littered with if-else profile conditionals.

When creating your connector instance, you can specify which profile type is your 'primary' (e.g. 'WORK'). This enables additional options, such as the following:

profileCalendarDatabase.primary().getEvents();

profileCalendarDatabase.secondary().getEvents();

// Runs on all profiles if running on the primary, or just
// on the current profile if running on the secondary.
profileCalendarDatabase.suppliers().getEvents();

Cross Profile Types

Classes and interfaces which contain a method annotated @CrossProfile are referred to as Cross Profile Types.

The implementation of Cross Profile Types should be profile-independent, the profile they are running on. They are allowed to make calls to other methods and in general should work like they were running on a single profile. They will only have access to state in their own profile.

An example Cross Profile Type:

public class Calculator {
  @CrossProfile
  public int add(int a, int b) {
    return a + b;
  }
}

Class annotation

To provide the strongest API, you should specify the connector for each cross profile type, as so:

@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
  @CrossProfile
  public int add(int a, int b) {
    return a + b;
  }
}

This is optional but means that the generated API will be more specific on types and stricter on compile-time checking.

Interfaces

By annotating methods on an interface as @CrossProfile you are stating that there can be some implementation of this method which should be accessible across profiles.

You can return any implementation of a Cross Profile interface in a Cross Profile Provider and by doing so you are saying that this implementation should be accessible cross-profile. You don't need to annotate the implementation classes.

Cross Profile Providers

Every Cross Profile Type must be provided by a method annotated @CrossProfileProvider. These methods will be called each time a cross-profile call is made, so it is recommended that you maintain singletons for each type.

Constructor

A provider must have a public constructor which takes either no arguments or a single Context argument.

Provider Methods

Provider methods must take either no arguments or a single Context argument.

Dependency Injection

If you're using a dependency injection framework such as Dagger to manage dependencies, we recommend that you have that framework create your cross profile types as you usually would, and then inject those types into your provider class. The @CrossProfileProvider methods can then return those injected instances.

Profile Connector

Each Cross Profile Configuration must have a single Profile Connector, which is responsible for managing the connection to the other profile.

Default Profile Connector

If there is only one Cross Profile Configuration in a codebase, then you can avoid creating your own Profile Connector and use com.google.android.enterprise.connectedapps.CrossProfileConnector. This is the default used if none is specified.

When constructing the Cross Profile Connector, you can specify some options on the builder:

  • Scheduled Executor Service

    If you want to have control over the threads created by the SDK, use #setScheduledExecutorService(),

  • Binder

    If you have specific needs regarding profile binding, use #setBinder. This is likely only used by Device Policy Controllers.

Custom Profile Connector

You will need a custom profile connector to be able to set some configuration (using CustomProfileConnector) and will need one if you need multiple connectors in a single codebase (for example if you have multiple processes, we recommend one connector per process).

When creating a ProfileConnector it should look like:

@GeneratedProfileConnector
public interface MyProfileConnector extends ProfileConnector {
  public static MyProfileConnector create(Context context) {
    // Configuration can be specified on the builder
    return GeneratedMyProfileConnector.builder(context).build();
  }
}
  • serviceClassName

    To change the name of the service generated (which should be referenced in your AndroidManifest.xml), use serviceClassName=.

  • primaryProfile

    To specify the primary profile, use primaryProfile.

  • availabilityRestrictions

    To change the restrictions the SDK places on connections and profile availability, use availabilityRestrictions.

Device Policy Controllers

If your app is a Device Policy Controller, then you must specify an instance of DpcProfileBinder referencing your DeviceAdminReceiver.

If you are implementing your own profile connector:

@GeneratedProfileConnector
public interface DpcProfileConnector extends ProfileConnector {
  public static DpcProfileConnector get(Context context) {
    return GeneratedDpcProfileConnector.builder(context).setBinder(new
DpcProfileBinder(new ComponentName("com.google.testdpc",
"AdminReceiver"))).build();
  }
}

or using the default CrossProfileConnector:

CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();

Cross Profile Configuration

The @CrossProfileConfiguration annotation is used to link together all cross profile types using a connector in order to dispatch method calls correctly. To do this, we annotate a class with @CrossProfileConfiguration which points to every provider, like so:

@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}

This will validate that for all Cross Profile Types they have either the same profile connector or no connector specified.

  • serviceSuperclass

    By default, the generated service will use android.app.Service as the superclass. If you need a different class (which itself must be a subclass of android.app.Service) to be the superclass, then specify serviceSuperclass=.

  • serviceClass

    If specified, then no service will be generated. This must match the serviceClassName in the profile connector you are using. Your custom service should dispatch calls using the generated _Dispatcher class as such:

public final class TestProfileConnector_Service extends Service {
  private Stub binder = new Stub() {
    private final TestProfileConnector_Service_Dispatcher dispatcher = new
TestProfileConnector_Service_Dispatcher();

    @Override
    public void prepareCall(long callId, int blockId, int numBytes, byte[] params)
{
      dispatcher.prepareCall(callId, blockId, numBytes, params);
    }

    @Override
    public byte[] call(long callId, int blockId, long crossProfileTypeIdentifier,
int methodIdentifier, byte[] params,
    ICrossProfileCallback callback) {
      return dispatcher.call(callId, blockId, crossProfileTypeIdentifier,
methodIdentifier, params, callback);
    }

    @Override
    public byte[] fetchResponse(long callId, int blockId) {
      return dispatcher.fetchResponse(callId, blockId);
  };

  @Override
  public Binder onBind(Intent intent) {
    return binder;
  }
}

This can be used if you need to perform additional actions before or after a cross-profile call.

  • Connector

    If you are using a connector other than the default CrossProfileConnector, then you must specify it using connector=.

Visibility

Every part of your application which interacts cross-profile must be able to see your Profile Connector.

Your @CrossProfileConfiguration annotated class must be able to see every provider used in your application.

Synchronous Calls

The Connected Apps SDK supports synchronous (blocking) calls for cases where they are unavoidable. However, there are a number of disadvantages to using these calls (such as the potential for calls to block for a long time) so it is recommended that you avoid synchronous calls when possible. For using asynchronous calls see Asynchronous calls .

Connection Holders

If you are using synchronous calls, then you must ensure that there is a connection holder registered before making cross profile calls, otherwise an exception will be thrown. For more information see Connection Holders.

To add a connection holder, call ProfileConnector#addConnectionHolder(Object) with any object (potentially, the object instance which is making the cross-profile call). This will record that this object is making use of the connection and will attempt to make a connection. This must be called before any synchronous calls are made. This is a non-blocking call so it is possible that the connection won't be ready (or may not be possible) by the time you make your call, in which case the usual error handling behaviour applies.

If you lack the appropriate cross-profile permissions when you call ProfileConnector#addConnectionHolder(Object) or no profile is available to connect, then no error will be thrown but the connected callback will never be called. If the permission is later granted or the other profile becomes available then the connection will be made then and the callback called.

Alternatively, ProfileConnector#connect(Object) is a blocking method which will add the object as a connection holder and either establish a connection or throw an UnavailableProfileException. This method can not be called from the UI Thread.

Calls to ProfileConnector#connect(Object) and the similar ProfileConnector#connect return auto-closing objects which will automatically remove the connection holder once closed. This allows for usage such as:

try (ProfileConnectionHolder p = connector.connect()) {
  // Use the connection
}

Once you are finished making synchronous calls, you should call ProfileConnector#removeConnectionHolder(Object). Once all connection holders are removed, the connection will be closed.

Connectivity

A connection listener can be used to be informed when the connection state changes, and connector.utils().isConnected can be used to determine if a connection is present. For example:

// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
  if (crossProfileConnector.utils().isConnected()) {
    // Make cross-profile calls.
  }
});

Asynchronous Calls

Every method exposed across the profile divide must be designated as blocking (synchronous) or non-blocking (asynchronous). Any method which returns an asynchronous data type (e.g. a ListenableFuture) or accepts a callback parameter is marked as non-blocking. All other methods are marked as blocking.

Asynchronous calls are recommended. If you must use synchronous calls see Synchronous Calls.

Callbacks

The most basic type of non-blocking call is a void method which accepts as one of its parameters an interface which contains a method to be called with the result. To make these interfaces work with the SDK, the interface must be annotated @CrossProfileCallback. For example:

@CrossProfileCallback
public interface InstallationCompleteListener {
  void installationComplete(int state);
}

This interface can then be used as a parameter in a @CrossProfile annotated method and be called as usual. For example:

@CrossProfile
public void install(String filename, InstallationCompleteListener callback) {
  // Do something on a separate thread and then:
  callback.installationComplete(1);
}

// In the mediator
profileInstaller.work().install(filename, (status) -> {
  // Deal with callback
}, (exception) -> {
  // Deal with possibility of profile unavailability
});

If this interface contains a single method, which takes either zero or one parameters, then it can also be used in calls to multiple profiles at once.

Any number of values can be passed using a callback, but the connection will only be held open for the first value. See Connection Holders for information on holding the connection open to receive more values.

Synchronous methods with callbacks

One unusual feature of using callbacks with the SDK is that you could technically write a synchronous method which uses a callback:

public void install(InstallationCompleteListener callback) {
  callback.installationComplete(1);
}

In this case, the method is actually synchronous, despite the callback. This code would execute correctly:

System.out.println("This prints first");
installer.install(() -> {
        System.out.println("This prints second");
});
System.out.println("This prints third");

However, when called using the SDK, this won't behave in the same way. There is no guarantee that the install method will have been called before "This prints third" is printed. Any uses of a method marked as asynchronous by the SDK must make no assumptions about when the method will be called.

Simple Callbacks

"Simple callbacks" are a more restrictive form of callback which allows for additional features when making cross-profile calls. Simple interfaces must contain a single method, which can take either zero or one parameters.

You can enforce that a callback interface must remain by specifying simple=true in the @CrossProfileCallback annotation.

Simple callbacks are usable with various methods like .both(), .suppliers(), and others.

Connection Holders

When making an asynchronous call (using either callbacks or futures) a connection holder will be added when making the call and removed when either an exception or a value is passed.

If you expect more than one result to be passed using a callback, you should manually add the callback as a connection holder:

MyCallback b = //...
connector.addConnectionHolder(b);

  profileMyClass.other().registerListener(b);

  // Now the connection will be held open indefinitely, once finished:
  connector.removeConnectionHolder(b);

This can also be used with a try-with-resources block:

MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
  profileMyClass.other().registerListener(b);

  // Other things running while we expect results
}

If we make a call with a callback or future, the connection will be held open until a result is passed. If we determine that a result won't be passed, then we should remove the callback or future as a connection holder:

connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);

For more information, see Connection Holders.

Futures

Futures are also supported natively by the SDK. The only natively supported Future type is ListenableFuture, though custom future types can be used. To use futures you just declare a supported Future type as the return type of a cross profile method and then use it as normal.

This has the same "unusual feature" as callbacks, where a synchronous method which returns a future (e.g. using immediateFuture) will behave differently when run on the current profile versus run on another profile. Any uses of a method marked as asynchronous by the SDK must make no assumptions about when the method will be called.

Threads

Don't block on the result of a cross-profile future or callback on the main thread. If you do this, then in some situations your code will block indefinitely. This is because the connection to the other profile is also established on the main thread, which will never occur if it is blocked pending a cross-profile result.

Availability

Availability listener can be used to be informed when the availability state changes, and connector.utils().isAvailable can be used to determine if another profile is available for use. For example:

crossProfileConnector.registerAvailabilityListener(() -> {
  if (crossProfileConnector.utils().isAvailable()) {
    // Show cross-profile content
  } else {
    // Hide cross-profile content
  }
});

Connection Holders

Connection holders are arbitrary objects which are recorded as having and interest in the cross-profile connection being established and kept alive.

By default, when making asynchronous calls, a connection holder will be added when the call starts, and removed when any result or error occurs.

Connection Holders can also be added and removed manually to exert more control over the connection. Connection holders can be added using connector.addConnectionHolder, and removed using connector.removeConnectionHolder.

When there is at least one connection holder added, the SDK will attempt to maintain a connection. When there are zero connection holders added, the connection can be closed.

You must maintain a reference to any connection holder you add - and remove it when it is no longer relevant.

Synchronous calls

Before making synchronous calls, a connection holder should be added. This can be done using any object, though you must keep track of that object so it can be removed when you no longer need to make synchronous calls.

Asynchronous calls

When making asynchronous calls connection holders will be automatically managed so that the connection is open between the call and the first response or error. If you need the connection to survive beyond this (e.g. to receive multiple responses using a single callback) you should add the callback itself as a connection holder, and remove it once you no longer need to receive further data.

Error Handling

By default, any calls made to the other profile when the other profile is not available will result in an UnavailableProfileException being thrown (or passed into the Future, or error callback for an async call).

To avoid this, developers can use #both() or #suppliers() and write their code to deal with any number of entries in the resulting list (this will be 1 if the other profile is unavailable, or 2 if it is available).

Exceptions

Any unchecked exceptions which happen after a call to the current profile will be propagated as usual. This applies regardless of the method used to make the call (#current(), #personal, #both, etc.).

Unchecked exceptions which happen after a call to the other profile will result in a ProfileRuntimeException being thrown with the original exception as the cause. This applies regardless of the method used to make the call (#other(), #personal, #both, etc.).

ifAvailable

As an alternative to catching and dealing with UnavailableProfileException instances, you can use the .ifAvailable() method to provide a default value which will be returned instead of throwing an UnavailableProfileException.

For example:

profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);

Testing

To make your code testable, you should be injecting instances of your profile connector to any code which uses it (to check for profile availability, to manually connect, etc.). You should also be injecting instances of your profile aware types where they are used.

We provide fakes of your connector and types which can be used in tests.

First, add the test dependencies:

  testAnnotationProcessor
'com.google.android.enterprise.connectedapps:connectedapps-processor:1.1.2'
  testCompileOnly
'com.google.android.enterprise.connectedapps:connectedapps-testing-annotations:1.1.2'
  testImplementation
'com.google.android.enterprise.connectedapps:connectedapps-testing:1.1.2'

Then, annotate your test class with @CrossProfileTest, identifying the @CrossProfileConfiguration annotated class to be tested:

@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {

}

This will cause the generation of fakes for all types and connectors used in the configuration.

Create instances of those fakes in your test:

private final FakeCrossProfileConnector connector = new
FakeCrossProfileConnector();
private final NotesManager personalNotesManager = new NotesManager(); //
real/mock/fake
private final NotesManager workNotesManager = new NotesManager(); // real/mock/fake

private final FakeProfileNotesManager profileNotesManager =
  FakeProfileNotesManager.builder()
    .personal(personalNotesManager)
    .work(workNotesManager)
    .connector(connector)
    .build();

Set up the profile state:

connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();

Pass the fake connector and cross profile class into your code under test and then make calls.

Calls will be routed to the correct target - and exceptions will be thrown when making calls to disconnected or unavailable profiles.

Supported Types

The following types are supported with no extra effort on your part. These can be used as either arguments or return types for all cross-profile calls.

  • Primitives (byte, short, int, long, float, double, char, boolean),
  • Boxed Primitives (java.lang.Byte, java.lang.Short, java.lang.Integer, java.lang.Long, java.lang.Float, java.lang.Double, java.lang.Character, java.lang.Boolean, java.lang.Void),
  • java.lang.String,
  • Anything which implements android.os.Parcelable,
  • Anything which implements java.io.Serializable,
  • Single-dimension non-primitive arrays,
  • java.util.Optional,
  • java.util.Collection,
  • java.util.List,
  • java.util.Map,
  • java.util.Set,
  • android.util.Pair,
  • com.google.common.collect.ImmutableMap.

Any supported generic types (for example java.util.Collection) may have any supported type as their type parameter. For example:

java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>> is a valid type.

Futures

The following types are supported only as return types:

  • com.google.common.util.concurrent.ListenableFuture

Custom Parcelable Wrappers

If your type is not in the earlier list, first consider if it can be made to correctly implement either android.os.Parcelable or java.io.Serializable. If it cannot then see parcelable wrappers to add support for your type.

Custom Future Wrappers

If you want to use a future type which is not in the earlier list, see future wrappers to add support.

Parcelable Wrappers

Parcelable Wrappers are the way that the SDK adds support for non parcelable types which cannot be modified. The SDK includes wrappers for many types but if the type you need to use is not included you must write your own.

A Parcelable Wrapper is a class designed to wrap another class and make it parcelable. It follows a defined static contract and is registered with the SDK so it can be used to convert a given type into a parcelable type, and also extract that type from the parcelable type.

Annotation

The parcelable wrapper class must be annotated @CustomParcelableWrapper, specifying the wrapped class as originalType. For example:

@CustomParcelableWrapper(originalType=ImmutableList.class)

Format

Parcelable wrappers must implement Parcelable correctly, and must have a static W of(Bundler, BundlerType, T) method which wraps the wrapped type and a non-static T get() method which returns the wrapped type.

The SDK will use these methods to provide seamless support for the type.

Bundler

To allow for wrapping generic types (such as lists and maps), the of method is passed a Bundler which is capable of reading (using #readFromParcel) and writing (using #writeToParcel) all supported types to a Parcel, and a BundlerType which represents the declared type to be written.

Bundler and BundlerType instances are themselves parcelable, and should be written as part of the parcelling of the parcelable wrapper, so that it can be used when reconstructing the parcelable wrapper.

If the BundlerType represents a generic type, the type variables can be found by calling .typeArguments(). Each type argument is itself a BundlerType.

For an example, see ParcelableCustomWrapper:

public class CustomWrapper<F> {
  private final F value;

  public CustomWrapper(F value) {
    this.value = value;
  }
  public F value() {
    return value;
  }
}

@CustomParcelableWrapper(originalType = CustomWrapper.class)
public class ParcelableCustomWrapper<E> implements Parcelable {

  private static final int NULL = -1;
  private static final int NOT_NULL = 1;

  private final Bundler bundler;
  private final BundlerType type;
  private final CustomWrapper<E> customWrapper;

  /**
  *   Create a wrapper for a given {@link CustomWrapper}.
  *
  *   <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
  */
  public static <F> ParcelableCustomWrapper<F> of(
      Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {
    return new ParcelableCustomWrapper<>(bundler, type, customWrapper);
  }

  public CustomWrapper<E> get() {
    return customWrapper;
  }

  private ParcelableCustomWrapper(
      Bundler bundler, BundlerType type, CustomWrapper<E> customWrapper) {
    if (bundler == null || type == null) {
      throw new NullPointerException();
    }
    this.bundler = bundler;
    this.type = type;
    this.customWrapper = customWrapper;
  }

  private ParcelableCustomWrapper(Parcel in) {
    bundler = in.readParcelable(Bundler.class.getClassLoader());

    int presentValue = in.readInt();

    if (presentValue == NULL) {
      type = null;
      customWrapper = null;
      return;
    }

    type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
    BundlerType valueType = type.typeArguments().get(0);

    @SuppressWarnings("unchecked")
    E value = (E) bundler.readFromParcel(in, valueType);

    customWrapper = new CustomWrapper<>(value);
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeParcelable(bundler, flags);

    if (customWrapper == null) {
      dest.writeInt(NULL);
      return;
    }

    dest.writeInt(NOT_NULL);
    dest.writeParcelable(type, flags);
    BundlerType valueType = type.typeArguments().get(0);
    bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);
  }

  @Override
  public int describeContents() {
    return 0;
  }

  @SuppressWarnings("rawtypes")
  public static final Creator<ParcelableCustomWrapper> CREATOR =
    new Creator<ParcelableCustomWrapper>() {
      @Override
      public ParcelableCustomWrapper createFromParcel(Parcel in) {
        return new ParcelableCustomWrapper(in);
      }

      @Override
      public ParcelableCustomWrapper[] newArray(int size) {
        return new ParcelableCustomWrapper[size];
      }
    };
}

Register with the SDK

Once created, to use your custom parcelable wrapper you'll need to register it with the SDK.

To do this, specify parcelableWrappers={YourParcelableWrapper.class} in either a CustomProfileConnector annotation or a CrossProfile annotation on a class.

Future Wrappers

Future Wrappers are how the SDK adds support for futures across profiles. The SDK includes support for ListenableFuture by default, but for other Future types you may add support yourself.

A Future Wrapper is a class designed to wrap a specific Future type and make it available to the SDK. It follows a defined static contract and must be registered with the SDK.

Annotation

The future wrapper class must be annotated @CustomFutureWrapper, specifying the wrapped class as originalType. For example:

@CustomFutureWrapper(originalType=SettableFuture.class)

Format

Future wrappers must extend com.google.android.enterprise.connectedapps.FutureWrapper.

Future wrappers must have a static W create(Bundler, BundlerType) method which creates an instance of the wrapper. At the same time this should create an instance of the wrapped future type. This should be returned by a non-static T getFuture() method. The onResult(E) and onException(Throwable) methods must be implemented to pass the result or throwable to the wrapped future.

Future wrappers must also have a static void writeFutureResult(Bundler, BundlerType, T, FutureResultWriter<E>) method. This should register with the passed in future for results, and when a result is given, call resultWriter.onSuccess(value). If an exception is given, resultWriter.onFailure(exception) should be called.

Finally, future wrappers must also have a static T<Map<Profile, E>> groupResults(Map<Profile, T<E>> results) method which converts a map from profile to future, into a future of a map from profile to result. CrossProfileCallbackMultiMerger can be used to make this logic easier.

For example:

/** A basic implementation of the future pattern used to test custom future
wrappers. */
public class SimpleFuture<E> {
  public static interface Consumer<E> {
    void accept(E value);
  }
  private E value;
  private Throwable thrown;
  private final CountDownLatch countDownLatch = new CountDownLatch(1);
  private Consumer<E> callback;
  private Consumer<Throwable> exceptionCallback;

  public void set(E value) {
    this.value = value;
    countDownLatch.countDown();
    if (callback != null) {
      callback.accept(value);
    }
  }

  public void setException(Throwable t) {
    this.thrown = t;
    countDownLatch.countDown();
    if (exceptionCallback != null) {
      exceptionCallback.accept(thrown);
    }
  }

  public E get() {
    try {
      countDownLatch.await();
    } catch (InterruptedException e) {
      eturn null;
    }
    if (thrown != null) {
      throw new RuntimeException(thrown);
    }
    return value;
  }

  public void setCallback(Consumer<E> callback, Consumer<Throwable>
exceptionCallback) {
    if (value != null) {
      callback.accept(value);
    } else if (thrown != null) {
      exceptionCallback.accept(thrown);
    } else {
      this.callback = callback;
      this.exceptionCallback = exceptionCallback;
    }
  }
}
/** Wrapper for adding support for {@link SimpleFuture} to the Connected Apps SDK.
*/
@CustomFutureWrapper(originalType = SimpleFuture.class)
public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {

  private final SimpleFuture<E> future = new SimpleFuture<>();

  public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType
bundlerType) {
    return new SimpleFutureWrapper<>(bundler, bundlerType);
  }

  private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {
    super(bundler, bundlerType);
  }

  public SimpleFuture<E> getFuture() {
    return future;
  }

  @Override
  public void onResult(E result) {
    future.set(result);
  }

  @Override
  public void onException(Throwable throwable) {
    future.setException(throwable);
  }

  public static <E> void writeFutureResult(
      SimpleFuture<E> future, FutureResultWriter<E> resultWriter) {

    future.setCallback(resultWriter::onSuccess, resultWriter::onFailure);
  }

  public static <E> SimpleFuture<Map<Profile, E>> groupResults(
      Map<Profile, SimpleFuture<E>> results) {
    SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();

    CrossProfileCallbackMultiMerger<E> merger =
        new CrossProfileCallbackMultiMerger<>(results.size(), m::set);
    for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {
      result
        .getValue()
        .setCallback(
          (value) -> merger.onResult(result.getKey(), value),
          (throwable) -> merger.missingResult(result.getKey()));
    }
    return m;
  }
}

Register with the SDK

Once created, to use your custom future wrapper you'll need to register it with the SDK.

To do this, specify futureWrappers={YourFutureWrapper.class} in either a CustomProfileConnector annotation or a CrossProfile annotation on a class.

Direct Boot mode

If your app supports direct boot mode , then you may need to make cross-profile calls before the profile is unlocked. By default, the SDK only allows connections when the other profile is unlocked.

To change this behaviour, if you are using a custom profile connector, you should specify availabilityRestrictions=AvailabilityRestrictions.DIRECT_BOOT_AWARE:

@GeneratedProfileConnector
@CustomProfileConnector(availabilityRestrictions=AvailabilityRestrictions.DIRECT_BO
OT_AWARE)
public interface MyProfileConnector extends ProfileConnector {
  public static MyProfileConnector create(Context context) {
    return GeneratedMyProfileConnector.builder(context).build();
  }
}

If you are using CrossProfileConnector, use .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT _AWARE on the builder.

With this change, you will be informed of availability, and able to make cross profile calls, when the other profile is not unlocked. It is your responsibility to ensure your calls only access device encrypted storage.