Расширенные темы

Эти разделы предназначены для справки, и вам не обязательно читать их сверху вниз.

Используйте API-интерфейсы платформы:

Эти API будут включены в SDK для более единообразной поверхности API (например, избегая объектов UserHandle), но на данный момент вы можете вызывать их напрямую.

Реализация проста: если вы можете взаимодействовать, вперед. Если нет, но вы можете запросить, покажите подсказку пользователя/баннер/подсказку/и т. д. Если пользователь согласен перейти в настройки, создайте намерение запроса и используйте Context#startActivity чтобы отправить туда пользователя. Вы можете либо использовать широковещательную рассылку, чтобы определить, когда эта способность изменится, либо просто проверить еще раз, когда пользователь вернется.

Чтобы проверить это, вам нужно открыть TestDPC в своем рабочем профиле, перейти в самый низ и выбрать добавление имени вашего пакета в список разрешенных подключенных приложений. Это имитирует включение вашего приложения в список разрешенных администратором.

Глоссарий

В этом разделе определены ключевые термины, связанные с разработкой межпрофильных проектов.

Конфигурация перекрестного профиля

Конфигурация перекрестного профиля группирует связанные классы поставщиков перекрестных профилей и обеспечивает общую конфигурацию функций перекрестного профиля. Обычно для каждой базы кода используется одна аннотация @CrossProfileConfiguration , но в некоторых сложных приложениях их может быть несколько.

Соединитель профиля

Соединитель управляет соединениями между профилями. Обычно каждый тип перекрестного профиля указывает на определенный соединитель. Каждый тип перекрестного профиля в одной конфигурации должен использовать один и тот же соединитель.

Класс поставщика перекрестных профилей

Класс поставщика перекрестных профилей группирует связанные типы перекрестных профилей.

Посредник

Посредник находится между кодом высокого и низкого уровня, распределяя вызовы по правильным профилям и объединяя результаты. Это единственный код, который должен учитывать профиль. Это архитектурная концепция, а не что-то встроенное в SDK.

Тип поперечного профиля

Тип перекрестного профиля — это класс или интерфейс, содержащий методы с аннотацией @CrossProfile . Код этого типа не обязательно должен учитывать профиль и в идеале должен действовать только на основе своих локальных данных.

Типы профилей

Тип профиля
Текущий Активный профиль, в котором мы выполняем.
Другой (если он существует) Профиль, который мы не выполняем.
Персональный Пользователь 0, профиль на устройстве, который невозможно отключить.
Работа Обычно пользователь 10, но может быть и выше, его можно включать и выключать, он используется для хранения рабочих приложений и данных.
Начальный Необязательно определяется приложением. Профиль, который показывает объединенное представление обоих профилей.
вторичный Если первичный определен, вторичным является профиль, который не является основным.
Поставщик Поставщиками основного профиля являются оба профиля, поставщиками вторичного профиля — только сам вторичный профиль.

Идентификатор профиля

Класс, представляющий тип профиля (личный или рабочий). Они будут возвращены методами, которые выполняются в нескольких профилях, и могут использоваться для запуска большего количества кода в этих профилях. Их можно сериализовать в int для удобного хранения.

В этом руководстве описаны рекомендуемые структуры для создания эффективных и обслуживаемых межпрофильных функций в вашем приложении Android.

Преобразуйте ваш CrossProfileConnector в синглтон.

На протяжении всего жизненного цикла вашего приложения следует использовать только один экземпляр, иначе вы создадите параллельные соединения. Это можно сделать либо с помощью фреймворка внедрения зависимостей, такого как Dagger, либо с помощью классического шаблона Singleton либо в новом классе, либо в существующем.

Внедрите или передайте сгенерированный экземпляр профиля в свой класс, когда вы делаете вызов, а не создаете его в методе.

Это позволит вам позже передать автоматически сгенерированный экземпляр 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 . Это значение по умолчанию, если ничего не указано.

При построении соединителя поперечного профиля вы можете указать некоторые параметры в конструкторе:

  • Плановые услуги исполнителя

    Если вы хотите контролировать потоки, созданные SDK, используйте #setScheduledExecutorService() ,

  • связующее

    Если у вас есть особые потребности в отношении привязки профиля, используйте #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 .

Контроллеры политики устройств

Если ваше приложение является контроллером политики устройств, вам необходимо указать экземпляр DpcProfileBinder ссылающийся на ваш DeviceAdminReceiver .

Если вы реализуете собственный соединитель профиля:

@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 Connected Apps поддерживает синхронные (блокирующие) вызовы в тех случаях, когда они неизбежны. Однако использование этих вызовов имеет ряд недостатков (например, вероятность блокировки вызовов на длительное время), поэтому рекомендуется по возможности избегать синхронных вызовов . Информацию об использовании асинхронных вызовов см. в разделе Асинхронные вызовы .

Держатели соединений

Если вы используете синхронные вызовы, перед выполнением межпрофильных вызовов необходимо убедиться, что зарегистрирован держатель соединения, в противном случае будет выдано исключение. Для получения дополнительной информации см. Держатели соединений.

Чтобы добавить держатель соединения, вызовите 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 это будет вести себя иначе. Нет никакой гарантии, что метод установки будет вызван до того, как будет напечатано сообщение «Это печатается третьим». При любом использовании метода, помеченного SDK как асинхронный, не должно быть предположений о том, когда этот метод будет вызван.

Простые обратные вызовы

«Простые обратные вызовы» — это более ограничительная форма обратного вызова, которая позволяет использовать дополнительные функции при выполнении межпрофильных вызовов. Простые интерфейсы должны содержать один метод, который может принимать ноль или один параметр.

Вы можете обеспечить сохранение интерфейса обратного вызова, указав simple=true в аннотации @CrossProfileCallback .

Простые обратные вызовы можно использовать с различными методами, такими как .both() , .suppliers() и другими.

Держатели соединений

При выполнении асинхронного вызова (с использованием обратных вызовов или фьючерсов) держатель соединения будет добавлен при выполнении вызова и удален при передаче исключения или значения.

Если вы ожидаете, что с помощью обратного вызова будет передано более одного результата, вам следует вручную добавить обратный вызов в качестве держателя соединения:

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
}

Если мы сделаем вызов с обратным вызовом или будущим вызовом, соединение будет оставаться открытым до тех пор, пока не будет передан результат. Если мы определим, что результат не будет передан, нам следует удалить обратный вызов или будущее как держатель соединения:

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

Для получения дополнительной информации см. Держатели соединений.

Фьючерсы

Фьючерсы также изначально поддерживаются SDK. Единственным изначально поддерживаемым типом Future является ListenableFuture , хотя можно использовать и собственные типы Future . Чтобы использовать фьючерсы, вы просто объявляете поддерживаемый тип Future как возвращаемый тип метода перекрестного профиля, а затем используете его как обычно.

Это имеет ту же «необычную особенность», что и обратные вызовы, где синхронный метод, возвращающий будущее (например, с использованием immediateFuture ), будет вести себя по-разному при запуске в текущем профиле и при запуске в другом профиле. При любом использовании метода, помеченного SDK как асинхронный, не должно быть предположений о том, когда этот метод будет вызван.

Темы

Не блокируйте результат межпрофильного будущего или обратного вызова в основном потоке . Если вы сделаете это, то в некоторых ситуациях ваш код будет блокироваться на неопределенный срок. Это связано с тем, что соединение с другим профилем также устанавливается в основном потоке, чего никогда не произойдет, если оно будет заблокировано в ожидании результата между профилями.

Доступность

Прослушиватель доступности можно использовать для получения информации об изменении состояния доступности, а connector.utils().isAvailable можно использовать для определения того, доступен ли для использования другой профиль. Например:

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

Держатели соединений

Держатели соединений — это произвольные объекты, которые регистрируются как имеющие и заинтересованные в установлении и поддержании межпрофильного соединения.

По умолчанию при выполнении асинхронных вызовов держатель соединения добавляется при запуске вызова и удаляется при возникновении какого-либо результата или ошибки.

Держатели соединений также можно добавлять и удалять вручную, чтобы обеспечить больший контроль над соединением. Держатели соединений можно добавить с помощью connector.addConnectionHolder и удалить с помощью connector.removeConnectionHolder .

Если добавлен хотя бы один держатель соединения, SDK попытается поддерживать соединение. Если добавлены держатели нулевых соединений, соединение можно закрыть.

Вы должны сохранить ссылку на любой добавленный вами держатель соединения и удалить его, когда он перестанет быть актуальным.

Синхронные звонки

Перед выполнением синхронных вызовов следует добавить держатель соединения. Это можно сделать с помощью любого объекта, однако вы должны отслеживать этот объект, чтобы его можно было удалить, когда вам больше не понадобится выполнять синхронные вызовы.

Асинхронные вызовы

При совершении асинхронных вызовов держатели соединений будут автоматически управляться таким образом, чтобы соединение было открыто между вызовом и первым ответом или ошибкой. Если вам нужно, чтобы соединение сохранялось и после этого (например, чтобы получать несколько ответов с использованием одного обратного вызова), вам следует добавить сам обратный вызов в качестве держателя соединения и удалить его, как только вам больше не нужно будет получать дополнительные данные.

Обработка ошибок

По умолчанию любые вызовы, сделанные к другому профилю, когда другой профиль недоступен, приведут к созданию исключения UnavailableProfileException (или передаче в будущее, или обратному вызову ошибки для асинхронного вызова).

Чтобы избежать этого, разработчики могут использовать #both() или #suppliers() и написать свой код для обработки любого количества записей в результирующем списке (это будет 1, если другой профиль недоступен, или 2, если он доступен). .

Исключения

Любые непроверенные исключения, возникающие после вызова текущего профиля, будут распространяться как обычно. Это применимо независимо от метода, использованного для вызова ( #current() , #personal , #both и т. д.).

Непроверенные исключения, возникающие после вызова другого профиля, приведут к созданию ProfileRuntimeException , причиной которого является исходное исключение. Это применимо независимо от метода, использованного для вызова ( #other() , #personal , #both и т. д.).

еслидоступно

В качестве альтернативы перехвату и обработке экземпляров 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();

Передайте поддельный соединитель и класс перекрестного профиля в тестируемый код, а затем совершайте вызовы.

Вызовы будут перенаправляться на правильную цель, а при вызовах к отключенным или недоступным профилям будут генерироваться исключения.

Поддерживаемые типы

Следующие типы поддерживаются без каких-либо дополнительных усилий с вашей стороны. Их можно использовать либо в качестве аргументов, либо в качестве возвращаемого типа для всех межпрофильных вызовов.

  • Примитивы ( byte , short , int , long , float , double , char , boolean ),
  • Примитивы в штучной упаковке ( 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 ,
  • Все, что реализует 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[]>> является допустимым типом.

Фьючерсы

Следующие типы поддерживаются только как возвращаемые типы:

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

Индивидуальная упаковка для посылок

Если вашего типа нет в предыдущем списке, сначала подумайте, можно ли его правильно реализовать либо android.os.Parcelable , либо java.io.Serializable . Если он не может, то просмотрите обертки для пакетов , чтобы добавить поддержку вашего типа.

Пользовательские оболочки будущего

Если вы хотите использовать будущий тип, которого нет в предыдущем списке, просмотрите будущие оболочки, чтобы добавить поддержку.

Упаковочные пакеты

Parcelable Wrappers — это способ, которым SDK добавляет поддержку неупаковываемых типов, которые нельзя изменить. SDK включает оболочки для многих типов, но если тип, который вам нужен, не включен, вам придется написать свой собственный.

Parcelable Wrapper — это класс, созданный для того, чтобы обернуть другой класс и сделать его упаковываемым. Он соответствует определенному статическому контракту и зарегистрирован в SDK, поэтому его можно использовать для преобразования данного типа в разделяемый тип, а также для извлечения этого типа из разделяемого типа.

Аннотация

Класс-оболочка для посылки должен быть аннотирован @CustomParcelableWrapper с указанием обернутого класса как originalType . Например:

@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`:

```java
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

После создания, чтобы использовать собственную обертку для посылок, вам необходимо зарегистрировать ее в SDK.

Для этого укажите parcelableWrappers={YourParcelableWrapper.class} либо в аннотации CustomProfileConnector , либо в аннотации CrossProfile класса.

Будущие обертки

Future Wrappers — это то, как SDK добавляет поддержку фьючерсов в разных профилях. SDK включает поддержку ListenableFuture по умолчанию, но для других типов Future вы можете добавить поддержку самостоятельно.

Future Wrapper — это класс, предназначенный для упаковки определенного типа Future и обеспечения его доступности для SDK. Он соответствует определенному статическому контракту и должен быть зарегистрирован в SDK.

Аннотация

Будущий класс-оболочка должен быть аннотирован @CustomFutureWrapper , указывая обернутый класс как originalType . Например:

@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:

```java
/** A very simple 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

После создания, чтобы использовать свою собственную будущую оболочку, вам необходимо зарегистрировать ее в SDK.

Для этого укажите futureWrappers={YourFutureWrapper.class} либо в аннотации CustomProfileConnector , либо в аннотации CrossProfile класса.

Режим прямой загрузки

Если ваше приложение поддерживает режим прямой загрузки , вам может потребоваться выполнить межпрофильные вызовы, прежде чем профиль будет разблокирован. По умолчанию 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 в сборщике.

Благодаря этому изменению вы будете проинформированы о доступности и сможете совершать вызовы между профилями, когда другой профиль не разблокирован. Вы несете ответственность за то, чтобы ваши звонки имели доступ только к зашифрованному хранилищу устройства.