Эти разделы предназначены для справки, и вам не обязательно читать их сверху вниз.
Запросить согласие пользователя
Используйте API-интерфейсы платформы:
-
CrossProfileApps.canInteractAcrossProfiles()
-
CrossProfileApps.canRequestInteractAcrossProfiles()
-
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
-
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
Эти 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
профиля, используйте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)
Формат
Оболочки Parcelable должны правильно реализовывать Parcelable
и иметь статический метод W of(Bundler, BundlerType, T)
, который обертывает обернутый тип, и нестатический метод T get()
, который возвращает обернутый тип.
SDK будет использовать эти методы для обеспечения бесперебойной поддержки этого типа.
Бандлер
Чтобы обеспечить перенос универсальных типов (таких как списки и карты), методу of
передается Bundler
, который способен читать (с помощью #readFromParcel
) и записывать (с помощью #writeToParcel
) все поддерживаемые типы в Parcel
, а также BundlerType
, который представляет объявленный тип для записи.
Экземпляры Bundler
и BundlerType
сами по себе являются пакетируемыми и должны быть написаны как часть пакетируемой оболочки, чтобы их можно было использовать при восстановлении пакетируемой оболочки.
Если 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
После создания, чтобы использовать собственную оболочку для посылок, вам необходимо зарегистрировать ее в SDK.
Для этого укажите parcelableWrappers={YourParcelableWrapper.class}
либо в аннотации CustomProfileConnector
, либо в аннотации CrossProfile
класса.
Будущие обертки
Future Wrappers — это то, как SDK добавляет поддержку фьючерсов в разных профилях. SDK включает поддержку ListenableFuture
по умолчанию, но для других типов Future вы можете добавить поддержку самостоятельно.
Future Wrapper — это класс, предназначенный для упаковки определенного типа Future и обеспечения его доступности для SDK. Он соответствует определенному статическому контракту и должен быть зарегистрирован в SDK.
Аннотация
Будущий класс-оболочка должен быть аннотирован @CustomFutureWrapper
, указывая обернутый класс как originalType
. Например:
@CustomFutureWrapper(originalType=SettableFuture.class)
Формат
Будущие оболочки должны расширять com.google.android.enterprise.connectedapps.FutureWrapper
.
Будущие оболочки должны иметь статический метод W create(Bundler, BundlerType)
, который создает экземпляр оболочки. В то же время это должно создать экземпляр завернутого будущего типа. Это значение должно быть возвращено нестатическим методом T
getFuture()
. Методы onResult(E)
и onException(Throwable)
должны быть реализованы для передачи результата или объекта throw в обернутое будущее.
Будущие оболочки также должны иметь статический метод void writeFutureResult(Bundler,
BundlerType, T, FutureResultWriter<E>)
. Это должно регистрироваться с переданными в будущем результатами, и когда результат будет получен, вызовите resultWriter.onSuccess(value)
. Если задано исключение, следует вызвать resultWriter.onFailure(exception)
.
Наконец, будущие оболочки также должны иметь статический метод T<Map<Profile, E>>
groupResults(Map<Profile, T<E>> results)
, который преобразует карту из профиля в будущее, в будущее карты из профиля в будущее. результат. 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
После создания, чтобы использовать свою собственную будущую оболочку, вам необходимо зарегистрировать ее в 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
в сборщике.
Благодаря этому изменению вы будете проинформированы о доступности и сможете совершать вызовы между профилями, когда другой профиль не разблокирован. Вы несете ответственность за то, чтобы ваши звонки имели доступ только к зашифрованному хранилищу устройства.
,Эти разделы предназначены для справки, и вам не обязательно читать их сверху вниз.
Запросить согласие пользователя
Используйте API-интерфейсы платформы:
-
CrossProfileApps.canInteractAcrossProfiles()
-
CrossProfileApps.canRequestInteractAcrossProfiles()
-
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
-
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
Эти 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 Profile Conditionals.
При создании экземпляра Connector вы можете указать, какой тип профиля является вашим «первичным» (например, «работа»). Это позволяет дополнительные параметры, такие как следующие:
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
.
Инъекция зависимости
Если вы используете структуру впрыска в зависимости, такую как кинжал, для управления зависимостями, мы рекомендуем, чтобы у вас была эта структура создавать ваши кросс -профиль, как обычно, а затем введите эти типы в свой класс поставщиков. Методы @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
), чтобы быть SuperClass, то указайте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
return overate автоматически закрывают объекты, которые автоматически удаляют держатель подключения после закрытия. Это позволяет использовать такое использование, как:
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. Единственным изначально поддерживаемым будущим типом является ListenableFuture
, хотя могут использоваться пользовательские будущие типы . Чтобы использовать будущее, вы просто объявляете поддерживаемый будущий тип как тип возврата метода перекрестного профиля, а затем используете его как обычно.
Это имеет ту же «необычную функцию», что и обратные вызовы, где синхронный метод, который возвращает будущее (например, с использованием 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, если он доступен) .
Исключения
Любые неконтролируемые исключения, которые происходят после вызова текущего профиля, будут распространяться как обычно. Это применимо независимо от метода, используемого для создания Call ( #current()
, #personal
, #both
и т. Д.).
Неконтролируемые исключения, которые происходят после вызова в другом профиле, приведут к тому, что ProfileRuntimeException
будет выброшено с первоначальным исключением в качестве причины. Это применимо независимо от метода, используемого для создания вызова ( #other()
, #personal
, #both
и т. Д.).
можно
В качестве альтернативы ловке и работе с UnavailableProfileException
экземплярами ProfileException вы можете использовать метод .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
. Если это не может, то увидеть приподнятые обертки , чтобы добавить поддержку вашего типа.
Пользовательские будущие обертки
Если вы хотите использовать будущий тип, которого нет в предыдущем списке, см. Future Frappers , чтобы добавить поддержку.
Приподнятые обертки
Причинные обертки - это способ, которым SDK добавляет поддержку не прибываемых типов, которые не могут быть изменены. SDK включает в себя обертки для многих типов, но если тип, который вам нужно использовать, не включен, вы должны написать свой собственный.
Обоснованная обертка - это класс, предназначенный для того, чтобы обернуть другой класс и сделать его приподнятым. Он следует определенному статическому контракту и зарегистрирован в SDK, поэтому его можно использовать для преобразования заданного типа в приподнятый тип, а также извлечь этот тип из приподнятого типа.
Аннотация
Класс средств для обертки должен быть аннотирован @CustomParcelableWrapper
, указывающий обернутый класс как originalType
. Например:
@CustomParcelableWrapper(originalType=ImmutableList.class)
Формат
Причинные обертки должны правильно Parcelable
, и должны иметь статический W of(Bundler, BundlerType, T)
, который завершает обернутый тип и нестатический метод T get()
, который возвращает обернутый тип.
SDK будет использовать эти методы для обеспечения беспроблемной поддержки для этого типа.
Будлер
Чтобы разрешить обертывание общих типов (таких как списки и карты), of
передается Bundler
, который способен прочитать (используя #readFromParcel
) и записать (с использованием #writeToParcel
) все поддерживаемые типы для Parcel
и BundlerType
, который представляет объявленный тип, который будет написан.
Экземпляры Bundler
и BundlerType
сами по себе являются посредством и должны быть записаны как часть проведения посылки для обертки, чтобы ее можно было использовать при реконструкции обертки.
Если 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
После создания, чтобы использовать пользовательскую обертку, вам нужно будет зарегистрировать ее с SDK.
Для этого укажите parcelableWrappers={YourParcelableWrapper.class}
либо в аннотации CustomProfileConnector
, либо в CrossProfile
аннотации на классе.
Будущие обертки
Будущие обертки - это то, как SDK добавляет поддержку фьючерсов по профилям. SDK включает в себя поддержку для ListenableFuture
по умолчанию, но для других будущих типов вы можете добавить поддержку себя.
Будущая обертка - это класс, предназначенный для того, чтобы обернуть конкретный будущий тип и сделать его доступным для SDK. Это следует за определенным статическим контрактом и должен быть зарегистрирован в SDK.
Аннотация
Класс будущего обертки должен быть аннотирован @CustomFutureWrapper
, указывающий обернутый класс как originalType
. Например:
@CustomFutureWrapper(originalType=SettableFuture.class)
Формат
Будущие обертки должны расширить com.google.android.enterprise.connectedapps.FutureWrapper
.
Будущие обертки должны иметь статический метод W create(Bundler, BundlerType)
, который создает экземпляр обертки. В то же время это должно создать экземпляр обернутого будущего. Это должно быть возвращено методом нестатического T
getFuture()
. Методы onResult(E)
и onException(Throwable)
должны быть реализованы, чтобы передать результат или выбрасываться в обернутое будущее.
Будущие обертки также должны иметь статический метод void writeFutureResult(Bundler,
BundlerType, T, FutureResultWriter<E>)
. Это должно зарегистрироваться с пропущенным в будущем для результатов, и когда будет дан результат, вызовите resultWriter.onSuccess(value)
. Если уделено исключение, следует вызвать resultWriter.onFailure(exception)
.
Наконец, будущие обертки также должны иметь статический метод T<Map<Profile, E>>
groupResults(Map<Profile, T<E>> results)
, который преобразует карту из профиля в будущее, в будущее карты от профиля в результат. 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
После создания, чтобы использовать пользовательскую будущую обертку, вам нужно будет зарегистрировать ее с 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
на строительном застройке.
С этим изменением вам будут информированы о доступности и сможете совершать перекрестные вызовы, когда другой профиль не разблокирован. Вы несете ответственность за то, чтобы ваши вызовы доступа только к зашифрованию устройства для устройства.