高级主题

这些部分仅供参考,您无需从头到尾阅读。

使用框架 API:

这些 API 将封装在 SDK 中,以实现更一致的 API Surface(例如,避免使用 UserHandle 对象),但目前,您可以直接调用这些 API。

实现方式非常简单:如果您可以互动,请继续操作。如果没有,但您可以请求,则显示用户提示/横幅/提示等。如果用户同意前往“设置”,请创建请求 intent 并使用 Context#startActivity 将用户发送到该位置。您可以使用广播来检测此 capability 何时发生变化,也可以在用户返回时再次进行检查。

如需进行此测试,您需要在工作资料中打开 TestDPC,然后滚动到底部,选择将您的软件包名称添加到已关联的应用许可名单。这模拟了管理员为您的应用“列入许可名单”的情况。

术语库

本部分定义了与开发跨资料应用相关的关键术语。

跨资料配置

跨资料配置会将相关的跨资料提供程序类分组在一起,并为跨资料功能提供常规配置。通常,每个代码库只有一个 @CrossProfileConfiguration 注解,但在某些复杂的应用中,可能有多个。

配置文件连接器

连接器用于管理配置文件之间的关联。通常,每种跨资料类型都指向特定的连接器。单个配置中的每个跨资料类型都必须使用相同的连接器。

跨资料提供程序类

跨资料提供程序类会将相关的跨资料类型归为一组。

Mediator

中介位于高级代码和低级代码之间,负责将调用分发到正确的配置文件并合并结果。这是唯一需要了解配置文件的代码。这是一种架构概念,而不是内置于 SDK 中的某种内容。

跨资料类型

跨资料类型是指包含带有 @CrossProfile 注解的方法的类或接口。此类中的代码无需了解配置文件,并且理想情况下应仅根据其本地数据执行操作。

配置文件类型

配置文件类型
当前我们正在执行的有效配置文件。
其他(如果存在)我们未在其上执行的配置文件。
个人用户 0,设备上无法关闭的个人资料。
工作通常为用户 10,但可能更高,可开启和关闭,用于包含工作应用和数据。
主要可由应用定义。显示这两个配置文件的合并视图的配置文件。
次要如果定义了主要配置文件,则次要配置文件是指非主要配置文件。
供应方主要付款资料的供应商是这两个付款资料,次要付款资料的供应商仅是次要付款资料本身。

个人资料标识符

表示个人资料类型(个人或工作)的类。这些方法将在多个配置文件上运行,并可用于在这些配置文件上运行更多代码。这些数据可以序列化为 int,以便于存储。

本指南概述了在 Android 应用中构建高效且可维护的跨资料功能的推荐结构。

CrossProfileConnector 转换为单例

在应用的整个生命周期内,应仅使用单个实例,否则您将创建并行连接。为此,您可以使用 Dagger 等依赖项注入框架,也可以在新类或现有类中使用传统的单例模式

在进行调用时,将生成的 Profile 实例注入或传入您的类,而不是在方法中创建该实例

这样,您就可以在后续的单元测试中传入自动生成的 FakeProfile 实例。

考虑使用中介模式

这种常见模式是,让现有 API 之一(例如 getEvents())对其所有调用方都具有性能剖析感知能力。在这种情况下,现有 API 可以直接成为包含对生成的跨资料代码的新调用的“中介”方法或类。

这样,您就不必强制要求每个调用方都知道如何进行跨资料调用,只需将其作为 API 的一部分即可。

考虑是否将接口方法注解为 @CrossProfile,以免在提供程序中公开实现类

这与依赖项注入框架非常契合。

如果您从跨资料调用收到任何数据,请考虑是否添加一个字段来引用数据来自哪个资料

这是一种很好的做法,因为您可能需要在界面层级了解这一点(例如,为工作内容添加标记图标)。如果没有此属性,任何数据标识符(例如软件包名称)都不再是唯一的,也可能需要此属性。

跨资料

本部分概述了如何构建自己的跨资料互动。

主要配置文件

本文档中示例中的大多数调用都包含有关在哪个资料(包括工作资料和个人资料)上运行的明确说明。

在实践中,对于仅在一个配置文件中提供合并体验的应用,您可能希望此决策取决于您所运行的配置文件,因此也有一些类似的便捷方法会考虑到这一点,以避免代码库中充斥着 if-else 配置文件条件。

创建连接器实例时,您可以指定哪种付款资料类型是您的“主要”付款资料(例如“工作”)。这样可以启用其他选项,例如:

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();

跨资料类型

包含带有 @CrossProfile 注解的方法的类和接口称为跨资料类型。

跨资料类型的实现应与运行这些类型的资料无关。它们可以调用其他方法,并且通常应像在单个配置文件中运行一样运行。他们只能访问自己个人资料中的状态。

跨资料类型示例:

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

类注解

为了提供最强大的 API,您应为每种跨资料类型指定连接器,如下所示:

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

这项设置是可选的,但意味着生成的 API 在类型方面会更具体,在编译时检查方面会更严格。

接口

通过将接口上的方法注解为 @CrossProfile,您表示此方法可能有某些实现,应可跨资料访问。

您可以在跨资料提供程序中返回跨资料接口的任何实现,这样做即表示此实现应可跨资料访问。您无需为实现类添加注解。

跨资料提供商

每个跨资料类型都必须由带有 @CrossProfileProvider 注解的方法提供。每次进行跨资料调用时,系统都会调用这些方法,因此建议您为每种类型维护单例。

构造函数

提供程序必须有一个不接受任何参数或仅接受一个 Context 参数的公共构造函数。

提供方方法

提供程序方法不得接受任何参数,或者只能接受一个 Context 参数。

依赖项注入

如果您使用 Dagger 等依赖项注入框架来管理依赖项,我们建议您让该框架像往常一样创建跨资料类型,然后将这些类型注入到提供程序类中。然后,@CrossProfileProvider 方法可以返回这些注入的实例。

配置文件连接器

每个跨资料配置都必须有一个资料连接器,该连接器负责管理与其他资料的连接。

默认配置文件连接器

如果代码库中只有一个跨资料配置,则可以避免创建自己的资料连接器,而使用 com.google.android.enterprise.connectedapps.CrossProfileConnector。如果未指定,则使用此默认值。

构建跨资料连接器时,您可以在构建器上指定一些选项:

  • Scheduled Executor Service

    如果您想控制 SDK 创建的线程,请使用 #setScheduledExecutorService()

  • Binder

    如果您对配置文件绑定有特定需求,请使用 #setBinder。这可能仅供设备政策控制器使用。

自定义配置文件连接器

您需要自定义配置文件连接器才能设置某些配置(使用 CustomProfileConnector),如果您需要在单个代码库中使用多个连接器,也需要自定义配置文件连接器(例如,如果您有多个进程,我们建议为每个进程使用一个连接器)。

创建 ProfileConnector 时,它应如下所示:

@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

    如需更改生成的服务的名称(应在 AndroidManifest.xml 中引用),请使用 serviceClassName=

  • primaryProfile

    如需指定主要个人资料,请使用 primaryProfile

  • availabilityRestrictions

    如需更改 SDK 对连接和配置文件可用性施加的限制,请使用 availabilityRestrictions

设备政策控制器

如果您的应用是设备政策控制器,则必须指定引用 DeviceAdminReceiverDpcProfileBinder 实例。

如果您要实现自己的配置文件连接器,请执行以下操作:

@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();
  }
}

或使用默认的 CrossProfileConnector

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

跨资料配置

@CrossProfileConfiguration 注解用于使用连接器将所有跨资料类型关联在一起,以便正确调度方法调用。为此,我们为指向每个提供程序的 @CrossProfileConfiguration 添加类注解,如下所示:

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

这将验证所有跨资料类型是否具有相同的资料连接器或未指定任何连接器。

  • serviceSuperclass

    默认情况下,生成的服务将使用 android.app.Service 作为父类。如果您需要将其他类(本身必须是 android.app.Service 的子类)作为父类,请指定 serviceSuperclass=

  • serviceClass

    如果指定,则不会生成任何服务。此值必须与您使用的配置文件连接器中的 serviceClassName 一致。您的自定义服务应使用生成的 _Dispatcher 类调度调用,如下所示:

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;
  }
}

如果您需要在跨资料调用之前或之后执行其他操作,可以使用此方法。

  • 连接器

    如果您使用的连接器不是默认的 CrossProfileConnector,则必须使用 connector= 指定它。

公开范围

应用中进行跨资料交互的每个部分都必须能够看到您的资料连接器。

带有 @CrossProfileConfiguration 注解的类必须能够看到应用中使用的每个提供程序。

同步调用

在不可避免的情况下,关联的应用 SDK 支持同步(阻塞)调用。不过,使用这些调用存在诸多缺点(例如调用可能会长时间阻塞),因此建议您尽可能避免使用同步调用。如需了解如何使用异步调用,请参阅异步调用

连接持有者

如果您使用的是同步调用,则必须确保在进行跨资料调用之前注册了连接持有者,否则系统会抛出异常。如需了解详情,请参阅连接持有者。

如需添加连接持有器,请使用任何对象(可能是发出跨资料调用的对象实例)调用 ProfileConnector#addConnectionHolder(Object)。这将记录此对象正在使用连接,并会尝试建立连接。必须在进行任何同步调用之前调用此方法。这是一个非阻塞调用,因此在您进行调用时,连接可能尚未就绪(或可能无法就绪),在这种情况下,系统会应用常规的错误处理行为。

如果您在调用 ProfileConnector#addConnectionHolder(Object) 时缺少适当的跨资料权限,或者没有可用于连接的资料,则系统不会抛出错误,但永远不会调用已连接的回调。如果稍后授予了权限或其他配置文件变为可用,系统将建立连接并调用回调。

或者,ProfileConnector#connect(Object) 是一种阻塞方法,它会将对象添加为连接持有者,并建立连接或抛出 UnavailableProfileException此方法无法从界面线程 调用

ProfileConnector#connect(Object) 和类似 ProfileConnector#connect 的调用会返回自动关闭的对象,这些对象会在关闭后自动移除连接持有器。这支持以下用法:

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

完成同步调用后,您应调用 ProfileConnector#removeConnectionHolder(Object)。移除所有连接持有者后,连接将关闭。

连接

连接监听器可用于在连接状态发生变化时接收通知,connector.utils().isConnected 可用于确定是否存在连接。例如:

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

异步调用

在配置文件分隔处公开的每个方法都必须指定为阻塞(同步)或非阻塞(异步)。任何返回异步数据类型(例如 ListenableFuture)或接受回调参数的方法都会被标记为非阻塞方法。所有其他方法均标记为阻塞。

建议使用异步调用。如果您必须使用同步调用,请参阅同步调用

回调

最基本的非阻塞调用类型是 void 方法,它接受一个接口作为参数,该接口包含要使用结果调用的方法。为了让这些接口与 SDK 搭配使用,必须为接口添加 @CrossProfileCallback 注解。例如:

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

然后,此接口可用作 @CrossProfile 注解方法中的参数,并照常调用。例如:

@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
});

如果此接口包含一个方法,且该方法接受零个或一个参数,则它还可用于一次调用多个配置文件。

您可以使用回调传递任意数量的值,但连接只会针对第一个值保持打开状态。如需了解如何保持连接打开状态以接收更多值,请参阅连接持有器。

带回调的同步方法

与 SDK 搭配使用回调的一个不寻常之处在于,您在技术上可以编写使用回调的同步方法:

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

在这种情况下,尽管有回调,该方法实际上是同步的。以下代码将正确执行:

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

不过,使用 SDK 调用时,此方法的行为不会相同。无法保证在输出“This prints third”之前已调用 install 方法。对 SDK 标记为异步的方法的任何使用都不得对该方法的调用时间做出任何假设。

简单回调

“简单回调”是一种限制更为严格的回调形式,可在进行跨资料调用时使用其他功能。简单接口必须包含一个方法,该方法可以接受零个或一个参数。

您可以在 @CrossProfileCallback 注解中指定 simple=true,以强制要求保留回调接口。

简单回调可与 .both().suppliers() 等各种方法搭配使用。

连接持有者

进行异步调用(使用回调或 Future)时,系统会在进行调用时添加连接持有器,并在传递异常或值时移除该持有器。

如果您希望使用回调传递多个结果,则应手动将回调添加为连接持有器:

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

  profileMyClass.other().registerListener(b);

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

这也可以与 try-with-resources 块搭配使用:

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

  // Other things running while we expect results
}

如果我们使用回调或 Future 进行调用,连接将保持打开状态,直到传递结果为止。如果我们确定不会传递结果,则应移除回调或 Future 作为连接持有者:

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

如需了解详情,请参阅连接持有器。

Futures

SDK 还原生支持 Future。唯一的原生支持的 Future 类型是 ListenableFuture,但可以使用自定义 Future 类型。如需使用 Future,只需将受支持的 Future 类型声明为跨资料方法的返回值类型,然后像往常一样使用即可。

这与回调具有相同的“异常行为”,即返回 Future 的同步方法(例如使用 immediateFuture)在当前配置文件上运行与在其他配置文件上运行时会表现出不同的行为。在任何情况下,使用 SDK 标记为异步的方法时,都不得对该方法的调用时间做出任何假设。

Threads

请勿在主线程上阻塞跨资料 Future 或回调的结果。如果您这样做,在某些情况下,您的代码将无限期阻塞。这是因为与其他资料的连接也是在主线程上建立的,如果主线程被阻塞并等待跨资料结果,则永远不会发生这种情况。

可用性

可用性监听器可用于在有空状态发生变化时接收通知,connector.utils().isAvailable 可用于确定是否可以使用其他个人资料。例如:

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

连接持有者

连接持有者是指被记录为对建立和保持活跃状态的跨资料连接感兴趣的任意对象。

默认情况下,进行异步调用时,系统会在调用开始时添加连接持有器,并在发生任何结果或错误时将其移除。

您还可以手动添加和移除关联持有者,以便更好地控制关联。您可以使用 connector.addConnectionHolder 添加连接持有者,并使用 connector.removeConnectionHolder 移除连接持有者。

添加至少一个连接持有者后,SDK 会尝试维持连接。如果未添加任何连接持有者,则可以关闭连接。

您必须保留对您添加的任何连接持有者的引用,并在其不再相关时将其移除。

同步调用

在进行同步调用之前,应添加连接持有器。您可以使用任何对象来实现此目的,但必须跟踪该对象,以便在您不再需要进行同步调用时将其移除。

异步调用

进行异步调用时,系统会自动管理连接持有者,以便在调用和第一个响应或错误之间建立连接。如果您需要连接在之后保持有效(例如,使用单个回调接收多个响应),则应将回调本身添加为连接持有者,并在不再需要接收进一步数据时将其移除。

错误处理

默认情况下,如果其他配置文件不可用,对其他配置文件的任何调用都会导致抛出 UnavailableProfileException(或传递到 Future,或异步调用的错误回调)。

为避免这种情况,开发者可以使用 #both()#suppliers(),并编写代码来处理生成列表中的任意数量的条目(如果其他配置文件不可用,则为 1;如果可用,则为 2)。

异常

调用当前配置文件后发生的任何未经检查的异常都将照常传播。无论用于进行调用的具体方法(#current()#personal#both 等)如何,此规则都适用。

在调用其他配置文件后发生的未经检查的异常将导致系统抛出 ProfileRuntimeException,并将原始异常作为原因。无论调用时使用的方法(#other()#personal#both 等)如何,此规则都适用。

ifAvailable

除了捕获和处理 UnavailableProfileException 实例之外,您还可以使用 .ifAvailable() 方法提供将返回的默认值,而不是抛出 UnavailableProfileException

例如:

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

测试

为了使代码可测试,您应将配置文件连接器的实例注入到使用它的任何代码(用于检查配置文件可用性、手动连接等)。您还应在使用配置文件感知型类型的位置注入这些类型的实例。

我们会提供可在测试中使用的连接器和类型的虚构对象。

首先,添加测试依赖项:

  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'

然后,使用 @CrossProfileTest 为测试类添加注解,指明要测试的添加了 @CrossProfileConfiguration 注解的类:

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

}

这会导致为配置中使用的所有类型和连接器生成虚构对象。

在测试中创建这些虚构实例:

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();

设置配置文件状态:

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

将虚构连接器和跨资料类传入要测试的代码,然后进行调用。

系统会将调用转送到正确的目标,并且在向已断开连接或不可用的个人资料进行调用时,会抛出异常。

支持的类型

系统支持以下类型,您无需额外付出任何努力。这些类型可用作所有跨资料调用的参数或返回值。

  • 基元(byteshortintlongfloatdoublecharboolean),
  • 封装的基元(java.lang.Bytejava.lang.Shortjava.lang.Integerjava.lang.Longjava.lang.Floatjava.lang.Doublejava.lang.Characterjava.lang.Booleanjava.lang.Void),
  • java.lang.String
  • 实现 android.os.Parcelable 的任何内容
  • 实现 java.io.Serializable 的任何内容
  • 单维非基元数组,
  • java.util.Optional
  • java.util.Collection
  • java.util.List
  • java.util.Map
  • java.util.Set
  • android.util.Pair
  • com.google.common.collect.ImmutableMap

任何受支持的泛型类型(例如 java.util.Collection)都可以将任何受支持的类型用作类型形参。例如:

java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>> 是有效的类型。

Futures

以下类型仅作为返回值类型受支持:

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

自定义 Parcelable 封装容器

如果您的类型不在前面的列表中,请先考虑是否可以使其正确实现 android.os.Parcelablejava.io.Serializable。如果它找不到 Parcelable 封装容器,则无法添加对您类型的支持。

自定义 Future 封装容器

如果您想使用上文列表中未列出的 Future 类型,请参阅 Future 封装容器以添加支持。

Parcelable 封装容器

Parcelable 封装容器是 SDK 为不可修改的非 Parcelable 类型添加支持的方式。SDK 包含许多类型的封装容器,但如果未包含您需要使用的类型,您必须自行编写。

Parcelable 封装容器是一种用于封装其他类并使其可分块传输的类。它遵循定义的静态协定并已向 SDK 注册,因此可用于将给定类型转换为 Parcelable 类型,还可从 Parcelable 类型中提取该类型。

注释

Parcelable 封装容器类必须带有 @CustomParcelableWrapper 注解,并将封装的类指定为 originalType。例如:

@CustomParcelableWrapper(originalType=ImmutableList.class)

格式

Parcelable 封装容器必须正确实现 Parcelable,并且必须有一个用于封装封装类型的静态 W of(Bundler, BundlerType, T) 方法,以及一个用于返回封装类型的非静态 T get() 方法。

SDK 将使用这些方法为该类型提供无缝支持。

Bundler

为了允许封装泛型(例如列表和映射),of 方法会传递一个 Bundler,该 Bundler 能够将所有受支持的类型读取(使用 #readFromParcel)和写入(使用 #writeToParcel)到 Parcel,以及一个 BundlerType,表示要写入的声明类型。

BundlerBundlerType 实例本身就是可分屏的,应在分屏 Parcelable 封装容器的过程中写入,以便在重构 Parcelable 封装容器时使用。

如果 BundlerType 表示泛型类型,则可以通过调用 .typeArguments() 找到类型变量。每个类型参数本身就是 BundlerType

如需查看示例,请参阅 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];
      }
    };
}

注册 SDK

创建自定义 Parcelable 封装容器后,您需要将其注册到 SDK 中才能使用。

为此,请在类的 CustomProfileConnector 注解或 CrossProfile 注解中指定 parcelableWrappers={YourParcelableWrapper.class}

Future 封装容器

Future 封装容器是 SDK 跨配置文件添加对 Future 的支持的方式。默认情况下,该 SDK 支持 ListenableFuture,但对于其他 Future 类型,您可以自行添加支持。

Future 封装容器是一种类,用于封装特定 Future 类型并将其提供给 SDK。它遵循定义的静态协定,并且必须向 SDK 注册。

注释

必须为 Future 封装容器类添加 @CustomFutureWrapper 注解,并将封装的类指定为 originalType。例如:

@CustomFutureWrapper(originalType=SettableFuture.class)

格式

Future 封装容器必须扩展 com.google.android.enterprise.connectedapps.FutureWrapper

未来的封装容器必须有一个静态 W create(Bundler, BundlerType) 方法,用于创建封装容器的实例。同时,这应该会创建封装 Future 类型的实例。这应由非静态 T getFuture() 方法返回。必须实现 onResult(E)onException(Throwable) 方法,才能将结果或可抛出对象传递给封装的 Future。

Future 封装容器还必须具有静态 void writeFutureResult(Bundler, BundlerType, T, FutureResultWriter<E>) 方法。这应与传入的 Future 注册以获取结果,并在有结果时调用 resultWriter.onSuccess(value)。如果指定了异常,则应调用 resultWriter.onFailure(exception)

最后,Future 封装容器还必须有一个静态 T<Map<Profile, E>> groupResults(Map<Profile, T<E>> results) 方法,用于将从配置文件到 Future 的映射转换为从配置文件到结果的映射的 Future。CrossProfileCallbackMultiMerger 可用于简化此逻辑。

例如:

/** 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;
  }
}

注册 SDK

创建后,如需使用自定义 Future 封装容器,您需要将其注册到 SDK。

为此,请在类的 CustomProfileConnector 注解或 CrossProfile 注解中指定 futureWrappers={YourFutureWrapper.class}

直接启动模式

如果您的应用支持直接启动模式,则您可能需要在解锁配置文件之前进行跨资料调用。默认情况下,SDK 仅在其他资料处于解锁状态时允许连接。

如需更改此行为,如果您使用的是自定义配置文件连接器,则应指定 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();
  }
}

如果您使用的是 CrossProfileConnector,请在构建器上使用 .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT _AWARE

此次变更生效后,当其他个人资料处于锁定状态时,您会收到可用性提醒,并且能够进行跨资料通话。您有责任确保您的调用仅访问设备加密存储空间。