Advanced topics

  • Use framework APIs likeCrossProfileApps to request user consent for cross-profile interaction.

  • Cross Profile development involves concepts like Configurations, Connectors, Provider Classes, Mediators, Types, and Profile Identifiers.

  • Recommended architectural solutions include makingCrossProfileConnector a singleton, injecting generated Profile instances, considering the mediator pattern, and annotating interface methods with@CrossProfile.

  • Cross Profile interactions involve defining primary profiles, creating Cross Profile Types and Providers, configuring a Profile Connector, and setting up a Cross Profile Configuration.

  • The SDK supports both synchronous and asynchronous calls, with asynchronous calls being recommended and managed using connection holders, callbacks, or futures.

  • Error handling for cross-profile calls can involveUnavailableProfileException or using.ifAvailable() for default values.

  • The SDK provides testing fakes and annotations like@CrossProfileTest to facilitate testing of cross-profile functionalities.

  • Various data types are supported for cross-profile calls, including primitives, Parcelables, and Serializables, with options for adding support for custom types using Parcelable and Future Wrappers.

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

Request user consent

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 useragrees to go to Settings, create the request intent and useContext#startActivity to send the user there. You can either use the broadcastto detect when this ability changes, or just check again when the user comesback.

To test this, you'll need to open TestDPC in your work profile, go to the verybottom and select to add your package name to the connected apps allowlist. Thismimics 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 ProviderClasses 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 profiletype will point to a specific Connector. Every cross profile type in a singleconfiguration 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 thecorrect profiles and merging results. This is the only code which needs to beprofile-aware. This is an architectural concept rather than something built intothe 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 shouldideally 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 bereturned by methods which run on multiple profiles and can be used to run morecode on those profiles. These can be serialised to anint for convenientstorage.

Architectural recommended solutions

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

Convert yourCrossProfileConnector into a singleton

Only a single instance should be used throughout the lifecycle of yourapplication, or else you will create parallel connections. This can be doneeither using a dependency injection framework such as Dagger, or by using aclassicSingleton pattern, either in a new class or anexisting 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-generatedFakeProfile instance inyour 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 justbecome a 'mediator' method or class that contains the new call to generatedcross-profile code.

This way, you don't force every caller to know to make a cross-profile call itjust 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 dataidentifiers 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 onwhich profiles to run on, including work, personal, and both.

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

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

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 arereferred to as Cross Profile Types.

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

An example Cross Profile Type:

publicclassCalculator{@CrossProfilepublicintadd(inta,intb){returna+b;}}

Class annotation

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

@CrossProfile(connector=MyProfileConnector.class)publicclassCalculator{@CrossProfilepublicintadd(inta,intb){returna+b;}}

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

Interfaces

By annotating methods on an interface as@CrossProfile you are stating thatthere can be some implementation of this method which should be accessibleacross profiles.

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

Cross Profile Providers

EveryCross Profile Type must be provided by a method annotated@CrossProfileProvider. These methods will be called each time a cross-profilecall 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 asingleContext argument.

Provider Methods

Provider methods must take either no arguments or a singleContext argument.

Dependency Injection

If you're using a dependency injection framework such as Dagger to managedependencies, we recommend that you have that framework create your crossprofile types as you usually would, and then inject those types into yourprovider class. The@CrossProfileProvider methods can then return thoseinjected instances.

Profile Connector

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

Note: The Profile Connector's connect method must be called before any blockinginteraction with the SDK. Seesynchronous calls. If you are only usingasynchronous calls , this does not apply to you.

Default Profile Connector

If there is only one Cross Profile Configuration in a codebase, then you canavoid creating your own Profile Connector and usecom.google.android.enterprise.connectedapps.CrossProfileConnector. This is thedefault used if none is specified.

When constructing the Cross Profile Connector, you can specify some options onthe 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. Thisis likely only used by Device Policy Controllers.

Custom Profile Connector

You will need a custom profile connector to be able to set some configuration(usingCustomProfileConnector) and will need one if you need multipleconnectors in a single codebase (for example if you have multiple processes, werecommend one connector per process).

When creating aProfileConnector it should look like:

@GeneratedProfileConnectorpublicinterfaceMyProfileConnectorextendsProfileConnector{publicstaticMyProfileConnectorcreate(Contextcontext){// Configuration can be specified on the builderreturnGeneratedMyProfileConnector.builder(context).build();}}
Important: You should use a single instance of your profile connector throughoutyour application. This can be managed by dagger or other dependency injectionframework, or by having a class maintain the singleton.

Device Policy Controllers

If your app is a Device Policy Controller, then you must specify an instance ofDpcProfileBinder referencing yourDeviceAdminReceiver.

If you are implementing your own profile connector:

@GeneratedProfileConnectorpublicinterfaceDpcProfileConnectorextendsProfileConnector{publicstaticDpcProfileConnectorget(Contextcontext){returnGeneratedDpcProfileConnector.builder(context).setBinder(newDpcProfileBinder(newComponentName("com.google.testdpc","AdminReceiver"))).build();}}

or using the defaultCrossProfileConnector:

CrossProfileConnectorconnector=CrossProfileConnector.builder(context).setBinder(newDpcProfileBinder(newComponentName("com.google.testdpc","AdminReceiver"))).build();

Cross Profile Configuration

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

@CrossProfileConfiguration(providers={TestProvider.class})publicabstractclassTestApplication{}

This will validate that for allCross Profile Types they have either thesame profile connector or no connector specified.

  • serviceSuperclass

    By default, the generated service will useandroid.app.Service as thesuperclass. If you need a different class (which itself must be a subclassofandroid.app.Service) to be the superclass, then specifyserviceSuperclass=.

  • serviceClass

    If specified, then no service will be generated. This must match theserviceClassName in the profile connector you are using. Your customservice should dispatch calls using the generated_Dispatcher class assuch:

publicfinalclassTestProfileConnector_ServiceextendsService{privateStubbinder=newStub(){privatefinalTestProfileConnector_Service_Dispatcherdispatcher=newTestProfileConnector_Service_Dispatcher();@OverridepublicvoidprepareCall(longcallId,intblockId,intnumBytes,byte[]params){dispatcher.prepareCall(callId,blockId,numBytes,params);}@Overridepublicbyte[]call(longcallId,intblockId,longcrossProfileTypeIdentifier,intmethodIdentifier,byte[]params,ICrossProfileCallbackcallback){returndispatcher.call(callId,blockId,crossProfileTypeIdentifier,methodIdentifier,params,callback);}@Overridepublicbyte[]fetchResponse(longcallId,intblockId){returndispatcher.fetchResponse(callId,blockId);};@OverridepublicBinderonBind(Intentintent){returnbinder;}}

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

  • Connector

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

Visibility

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

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

Synchronous Calls

The Connected Apps SDK supports synchronous (blocking) calls for cases wherethey are unavoidable. However, there are a number of disadvantages to usingthese calls (such as the potential for calls to block for a long time) so it isrecommended that youavoid synchronous calls when possible. For usingasynchronous calls seeAsynchronous calls .

Connection Holders

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

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

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

Alternatively,ProfileConnector#connect(Object) is a blocking method whichwill add the object as a connection holder and either establish a connection orthrow anUnavailableProfileException.This method can not be called fromthe UI Thread.

Calls toProfileConnector#connect(Object) and the similarProfileConnector#connect return auto-closing objects which will automaticallyremove the connection holder once closed. This allows for usage such as:

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

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

Connectivity

A connection listener can be used to be informed when the connection statechanges, andconnector.utils().isConnected can be used to determine if aconnection 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 anasynchronous data type (e.g. aListenableFuture) or accepts a callbackparameter is marked as non-blocking. All other methods are marked as blocking.

Asynchronous calls are recommended. If you must use synchronous calls seeSynchronous Calls.

Callbacks

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

@CrossProfileCallbackpublicinterfaceInstallationCompleteListener{voidinstallationComplete(intstate);}

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

@CrossProfilepublicvoidinstall(Stringfilename,InstallationCompleteListenercallback){// Do something on a separate thread and then:callback.installationComplete(1);}// In the mediatorprofileInstaller.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 oneparameters, 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 willonly be held open for the first value. See Connection Holders for information onholding the connection open to receive more values.

Synchronous methods with callbacks

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

publicvoidinstall(InstallationCompleteListenercallback){callback.installationComplete(1);}

In this case, the method is actually synchronous, despite the callback. Thiscode 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 isno guarantee that the install method will have been called before "This printsthird" is printed. Any uses of a method marked as asynchronous by the SDK mustmake no assumptions about when the method will be called.

Simple Callbacks

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

You can enforce that a callback interface must remain by specifyingsimple=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) aconnection holder will be added when making the call and removed when either anexception or a value is passed.

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

MyCallbackb=//...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:

MyCallbackb=//...try(ProfileConnectionHolderp=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 openuntil a result is passed. If we determine that a result won't be passed, then weshould 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 supportedFuture type isListenableFuture, thoughcustom future types can be used.To use futures you just declare a supported Future type as the return type of across profile method and then use it as normal.

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

Threads

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

Availability

Availability listener can be used to be informed when the availability statechanges, andconnector.utils().isAvailable can be used to determine if anotherprofile 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 andinterest in the cross-profile connection being established and kept alive.

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

Connection Holders can also be added and removed manually to exert more controlover the connection. Connection holders can be added usingconnector.addConnectionHolder, and removed usingconnector.removeConnectionHolder.

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

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

Synchronous calls

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

Asynchronous calls

When making asynchronous calls connection holders will be automatically managedso 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 multipleresponses using a single callback) you should add the callback itself as aconnection holder, and remove it once you no longer need to receive furtherdata.

Error Handling

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

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

Exceptions

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

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

Note: Checked exceptions are not supported in Cross Profile methods.

ifAvailable

As an alternative to catching and dealing withUnavailableProfileExceptioninstances, you can use the.ifAvailable() method to provide a default valuewhich will be returned instead of throwing anUnavailableProfileException.

For example:

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

Testing

To make your code testable, you should be injecting instances of your profileconnector to any code which uses it (to check for profile availability, tomanually connect, etc.). You should also be injecting instances of your profileaware 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)publicclassNotesMediatorTest{}

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

Create instances of those fakes in your test:

privatefinalFakeCrossProfileConnectorconnector=newFakeCrossProfileConnector();privatefinalNotesManagerpersonalNotesManager=newNotesManager();//real/mock/fakeprivatefinalNotesManagerworkNotesManager=newNotesManager();// real/mock/fakeprivatefinalFakeProfileNotesManagerprofileNotesManager=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 andthen make calls.

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

Supported Types

The following types are supported with no extra effort on your part. These canbe 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 implementsandroid.os.Parcelable,
  • Anything which implementsjava.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 examplejava.util.Collection) may have anysupported type as their type parameter. For example:

java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>> isa 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 tocorrectly implement eitherandroid.os.Parcelable orjava.io.Serializable. Ifit cannot then seeparcelable 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, seefuturewrappers to add support.

Parcelable Wrappers

Parcelable Wrappers are the way that the SDK adds support for non parcelabletypes which cannot be modified. The SDK includes wrappers for manytypesbut 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 itparcelable. It follows a defined static contract and is registered with the SDKso it can be used to convert a given type into a parcelable type, and alsoextract that type from the parcelable type.

Important: In this section we will refer to the wrapped type asT, and thewrapper type asW.

Annotation

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

@CustomParcelableWrapper(originalType=ImmutableList.class)

Format

Parcelable wrappers must implementParcelable correctly, and must have astaticW of(Bundler, BundlerType, T) method which wraps the wrapped type and anon-staticT 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), theof method ispassed aBundler which is capable of reading (using#readFromParcel) andwriting (using#writeToParcel) all supported types to aParcel, and aBundlerType which represents the declared type to be written.

Bundler andBundlerType instances are themselves parcelable, and should bewritten as part of the parcelling of the parcelable wrapper, so that it can beused when reconstructing the parcelable wrapper.

If theBundlerType represents a generic type, the type variables can be foundby calling.typeArguments(). Each type argument is itself aBundlerType.

For an example, seeParcelableCustomWrapper:

publicclassCustomWrapper<F>{privatefinalFvalue;publicCustomWrapper(Fvalue){this.value=value;}publicFvalue(){returnvalue;}}@CustomParcelableWrapper(originalType=CustomWrapper.class)publicclassParcelableCustomWrapper<E>implementsParcelable{privatestaticfinalintNULL=-1;privatestaticfinalintNOT_NULL=1;privatefinalBundlerbundler;privatefinalBundlerTypetype;privatefinalCustomWrapper<E>customWrapper;/**  *   Create a wrapper for a given {@link CustomWrapper}.  *  *   <p>The passed in {@link Bundler} must be capable of bundling {@code F}.  */publicstatic<F>ParcelableCustomWrapper<F>of(Bundlerbundler,BundlerTypetype,CustomWrapper<F>customWrapper){returnnewParcelableCustomWrapper<>(bundler,type,customWrapper);}publicCustomWrapper<E>get(){returncustomWrapper;}privateParcelableCustomWrapper(Bundlerbundler,BundlerTypetype,CustomWrapper<E>customWrapper){if(bundler==null||type==null){thrownewNullPointerException();}this.bundler=bundler;this.type=type;this.customWrapper=customWrapper;}privateParcelableCustomWrapper(Parcelin){bundler=in.readParcelable(Bundler.class.getClassLoader());intpresentValue=in.readInt();if(presentValue==NULL){type=null;customWrapper=null;return;}type=(BundlerType)in.readParcelable(Bundler.class.getClassLoader());BundlerTypevalueType=type.typeArguments().get(0);@SuppressWarnings("unchecked")Evalue=(E)bundler.readFromParcel(in,valueType);customWrapper=newCustomWrapper<>(value);}@OverridepublicvoidwriteToParcel(Parceldest,intflags){dest.writeParcelable(bundler,flags);if(customWrapper==null){dest.writeInt(NULL);return;}dest.writeInt(NOT_NULL);dest.writeParcelable(type,flags);BundlerTypevalueType=type.typeArguments().get(0);bundler.writeToParcel(dest,customWrapper.value(),valueType,flags);}@OverridepublicintdescribeContents(){return0;}@SuppressWarnings("rawtypes")publicstaticfinalCreator<ParcelableCustomWrapper>CREATOR=newCreator<ParcelableCustomWrapper>(){@OverridepublicParcelableCustomWrappercreateFromParcel(Parcelin){returnnewParcelableCustomWrapper(in);}@OverridepublicParcelableCustomWrapper[]newArray(intsize){returnnewParcelableCustomWrapper[size];}};}

Register with the SDK

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

To do this, specifyparcelableWrappers={YourParcelableWrapper.class} in eitheraCustomProfileConnector annotation or aCrossProfile annotation on a class.

Future Wrappers

Future Wrappers are how the SDK adds support for futures across profiles. TheSDK includes support forListenableFuture by default, but for other Futuretypes you may add support yourself.

A Future Wrapper is a class designed to wrap a specific Future type and make itavailable to the SDK. It follows a defined static contract and must beregistered with the SDK.

Important: In this section we will refer to the wrapped type asT, and thewrapper type asW.

Annotation

The future wrapper class must be annotated@CustomFutureWrapper, specifyingthe wrapped class asoriginalType. For example:

@CustomFutureWrapper(originalType=SettableFuture.class)

Format

Future wrappers must extendcom.google.android.enterprise.connectedapps.FutureWrapper.

Future wrappers must have a staticW create(Bundler, BundlerType) method whichcreates an instance of the wrapper. At the same time this should create aninstance of the wrapped future type. This should be returned by a non-staticTgetFuture() method. TheonResult(E) andonException(Throwable) methodsmust be implemented to pass the result or throwable to the wrapped future.

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

Finally, future wrappers must also have a staticT<Map<Profile, E>>groupResults(Map<Profile, T<E>> results) method which converts a map fromprofile 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 futurewrappers. */publicclassSimpleFuture<E>{publicstaticinterfaceConsumer<E>{voidaccept(Evalue);}privateEvalue;privateThrowablethrown;privatefinalCountDownLatchcountDownLatch=newCountDownLatch(1);privateConsumer<E>callback;privateConsumer<Throwable>exceptionCallback;publicvoidset(Evalue){this.value=value;countDownLatch.countDown();if(callback!=null){callback.accept(value);}}publicvoidsetException(Throwablet){this.thrown=t;countDownLatch.countDown();if(exceptionCallback!=null){exceptionCallback.accept(thrown);}}publicEget(){try{countDownLatch.await();}catch(InterruptedExceptione){eturnnull;}if(thrown!=null){thrownewRuntimeException(thrown);}returnvalue;}publicvoidsetCallback(Consumer<E>callback,Consumer<Throwable>exceptionCallback){if(value!=null){callback.accept(value);}elseif(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)publicfinalclassSimpleFutureWrapper<E>extendsFutureWrapper<E>{privatefinalSimpleFuture<E>future=newSimpleFuture<>();publicstatic<E>SimpleFutureWrapper<E>create(Bundlerbundler,BundlerTypebundlerType){returnnewSimpleFutureWrapper<>(bundler,bundlerType);}privateSimpleFutureWrapper(Bundlerbundler,BundlerTypebundlerType){super(bundler,bundlerType);}publicSimpleFuture<E>getFuture(){returnfuture;}@OverridepublicvoidonResult(Eresult){future.set(result);}@OverridepublicvoidonException(Throwablethrowable){future.setException(throwable);}publicstatic<E>voidwriteFutureResult(SimpleFuture<E>future,FutureResultWriter<E>resultWriter){future.setCallback(resultWriter::onSuccess,resultWriter::onFailure);}publicstatic<E>SimpleFuture<Map<Profile,E>>groupResults(Map<Profile,SimpleFuture<E>>results){SimpleFuture<Map<Profile,E>>m=newSimpleFuture<>();CrossProfileCallbackMultiMerger<E>merger=newCrossProfileCallbackMultiMerger<>(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()));}returnm;}}

Register with the SDK

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

To do this, specifyfutureWrappers={YourFutureWrapper.class} in either aCustomProfileConnector annotation or aCrossProfile annotation on a class.

Direct Boot mode

If your app supportsdirect 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, youshould specifyavailabilityRestrictions=AvailabilityRestrictions.DIRECT_BOOT_AWARE:

@GeneratedProfileConnector@CustomProfileConnector(availabilityRestrictions=AvailabilityRestrictions.DIRECT_BOOT_AWARE)publicinterfaceMyProfileConnectorextendsProfileConnector{publicstaticMyProfileConnectorcreate(Contextcontext){returnGeneratedMyProfileConnector.builder(context).build();}}

If you are usingCrossProfileConnector, use.setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT_AWARE onthe builder.

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

Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2024-11-15 UTC.