These sections are meant for reference and it is not required that you read them top-to-bottom.
Request user consent
Use framework APIs:
-
CrossProfileApps.canInteractAcrossProfiles()
-
CrossProfileApps.canRequestInteractAcrossProfiles()
-
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
-
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
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 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.
Architectural recommended solutions
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
), useserviceClassName=
. -
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 ofandroid.app.Service
) to be the superclass, then specifyserviceSuperclass=
. -
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 usingconnector=
.
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 beforeany 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.