Te sekcje są przeznaczone do celów informacyjnych i nie musisz czytać ich od góry do dołu.
Prośba o zgodę użytkownika
Używanie interfejsów API framework:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
Te interfejsy API zostaną zapakowane w pakiet SDK, aby zapewnić bardziej spójną powierzchnię interfejsu API (np. unikanie obiektów UserHandle), ale na razie możesz wywoływać je bezpośrednio.
Wdrożenie jest proste: jeśli możesz wchodzić w interakcje, rób to. Jeśli nie,
ale możesz poprosić, wyświetl prompt, baner, podpowiedź itp. Jeśli użytkownik zgodzi się przejść do Ustawień, utwórz prośbę o ustawienie i użyj Context#startActivity
, aby wysłać użytkownika do Ustawień. Możesz użyć transmisji, aby wykryć, kiedy ta funkcja się zmieni, lub po prostu sprawdzić ponownie, gdy użytkownik wróci.
Aby to przetestować, otwórz TestDPC w profilu służbowym, przejdź na sam dół i wybierz opcję dodania nazwy pakietu do listy dozwolonych połączonych aplikacji. To naśladuje umieszczenie aplikacji na liście dozwolonych przez administratora.
Słowniczek
W tej sekcji zdefiniowano kluczowe terminy związane z rozwojem opartym na wielu profilach.
Konfiguracja profilu połączonego
Konfiguracja profilu połączonego łączy ze sobą powiązane klasy dostawcy profilu połączonego i zapewnia ogólną konfigurację funkcji profilu połączonego.
Zazwyczaj na każdą bazę kodu przypada 1 annotacja @CrossProfileConfiguration
, ale w przypadku niektórych złożonych aplikacji może ich być więcej.
Łącznik profili
Połączenie między profilami zarządza usługa pośrednicząca. Zwykle każdy typ profilu będzie wskazywał konkretny Konektor. Każdy typ profilu krzyżowego w pojedynczej konfiguracji musi korzystać z tego samego łącznika.
Klasa dostawcy między profilami
Klasa dostawcy typu „cross profile” grupuje powiązane typy „cross profile”.
Mediator
Pośrednik znajduje się między kodem wysokiego poziomu a kodem niskiego poziomu, rozsyłając wywołania do odpowiednich profili i zliczając wyniki. Jest to jedyny kod, który musi uwzględniać profil. Jest to koncepcja architektury, a nie coś wbudowanego w SDK.
Typ profilu połączonego
Typ profilu krzyżowego to klasa lub interfejs zawierający metody oznaczone jako @CrossProfile
. Kod tego typu nie musi być zależny od profilu i powinien działać tylko na podstawie danych lokalnych.
Typy profili
Typ profilu | |
---|---|
Obecnie | Aktywny profil, na którym wykonujemy operację. |
Inne | (jeśli istnieje) Profil, który nie jest wykonywany. |
Prywatnie | Użytkownik 0, profil na urządzeniu, którego nie można wyłączyć. |
Praca | Zwykle jest to użytkownik 10, ale może być większa, można go włączać i wyłączać. Służy do przechowywania aplikacji i danych służbowych. |
Główna | Opcjonalnie zdefiniowane przez aplikację. Profil, który wyświetla złączony widok obu profili. |
Dodatkowy | Jeśli zdefiniowano profil główny, profilem dodatkowym jest profil, który nie jest profilem głównym. |
Dostawca | Dostawcy dla profilu głównego to oba profile, dostawcy dla profilu dodatkowego to tylko sam profil dodatkowy. |
Identyfikator profilu
Klasa reprezentująca typ profilu (osobisty lub służbowy). Te wartości są zwracane przez metody, które działają na wielu profilach i mogą być używane do uruchamiania większej ilości kodu na tych profilach. Można je zakodować w postaci int
, aby wygodnie je przechowywać.
Zalecane rozwiązania architektoniczne
Ten przewodnik zawiera zalecane struktury na potrzeby tworzenia wydajnych i łatwo utrzymywanych funkcji w aplikacji na Androida, które działają w różnych profilach.
Konwertowanie CrossProfileConnector
na singleton
W całym cyklu życia aplikacji należy używać tylko jednej instancji, ponieważ w przeciwnym razie powstaną połączenia równoległe. Można to zrobić za pomocą platformy do iniekcji zależności, takiej jak Dagger, lub klasycznego wzorca Singleton w nowej lub istniejącej klasie.
Wstrzyknij wygenerowaną instancję profilu do klasy, aby przekazać ją podczas wywołania, zamiast tworzyć ją w metodzie.
Dzięki temu możesz później przekazać automatycznie wygenerowaną instancję FakeProfile
w testach jednostkowych.
Rozważ użycie wzorca pośrednika
Ten typowy schemat polega na tym, że jeden z dotychczasowych interfejsów API (np. getEvents()
) jest dostępny dla wszystkich wywołujących go użytkowników. W takim przypadku istniejący interfejs API może stać się „mediatorem” (metodą lub klasą), który zawiera nowe wywołanie wygenerowanego kodu na potrzeby różnych profili.
Dzięki temu nie musisz zmuszać każdego dzwoniącego do korzystania z wywołań między profilami – staje się ono częścią Twojego interfejsu API.
Zastanów się, czy zamiast tego nie warto oznaczyć metody interfejsu jako @CrossProfile
, aby uniknąć konieczności ujawniania klas implementacji w dostawcy.
To rozwiązanie dobrze sprawdza się w ramkach wstrzykiwania zależności.
Jeśli otrzymujesz jakiekolwiek dane z poziomu wywołania na różnych profilach, rozważ dodanie pola wskazującego, z którego profilu pochodzą.
Może to być przydatne, ponieważ możesz chcieć wiedzieć, jak to wygląda w interfejsie (np. dodać ikonę plakietki do elementów służbowych). Może być ona też wymagana, jeśli bez niej identyfikatory danych, takie jak nazwy pakietów, nie są już unikalne.
Profil połączony
Z tej sekcji dowiesz się, jak tworzyć własne interakcje między profilami.
Profile główne
Większość wywołań w przykładach w tym dokumencie zawiera wyraźne instrukcje dotyczące tego, na których profilach mają być wykonywane, w tym na profilu służbowym, osobistym lub na obu.
W praktyce w przypadku aplikacji, które mają złączone funkcje na jednym profilu, prawdopodobnie chcesz, aby ta decyzja zależała od profilu, na którym działasz, więc istnieją podobne wygodne metody, które również to uwzględniają, aby baza kodu nie była wypełniona instrukcjami if-else zależnymi od profilu.
Podczas tworzenia instancji łącznika możesz określić, który typ profilu jest „głównym” (np. „PRACA”). Dzięki temu możesz korzystać z dodatkowych opcji, takich jak:
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();
Typy profili połączonych
Klasy i interfejsy, które zawierają metodę z adnotacją @CrossProfile
, są określane jako typy na potrzeby wielu profili.
Implementacja typów profili na wielu profilach powinna być niezależna od profilu, na którym są uruchamiane. Mogą one wywoływać inne metody i generalnie powinny działać tak, jakby były uruchamiane w ramach pojedynczego profilu. Będą mieć dostęp tylko do informacji zawartych w ich własnym profilu.
Przykład typu profilu połączonego:
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
Adnotacja zajęć
Aby zapewnić najmocniejsze API, należy określić łącznik dla każdego typu profilu krzyżowego:
@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
Jest to opcjonalne, ale oznacza, że wygenerowany interfejs API będzie bardziej szczegółowy w przypadku typów i ostrzejszy w przypadku sprawdzania w czasie kompilacji.
Interfejsy
Oznaczając metody w interfejsie jako @CrossProfile
, deklarujesz, że może istnieć implementacja tej metody, która powinna być dostępna w różnych profilach.
Możesz zwrócić dowolną implementację interfejsu Cross Profile w Cross Profile Provider, co oznacza, że ta implementacja powinna być dostępna na różnych profilach. Nie musisz dodawać adnotacji do klas implementacji.
Dostawcy danych międzyprofilowych
Każdy typ profilu krzyżowego musi być podany za pomocą metody oznaczonej adnotacją @CrossProfileProvider
. Te metody są wywoływane za każdym razem, gdy następuje wywołanie w ramach wielu profili, dlatego zalecamy utrzymywanie pojedynczych obiektów dla każdego typu.
Zespół
Usługodawca musi mieć publiczny konstruktor, który nie przyjmuje żadnych argumentów lub jeden argument Context
.
Metody dostawcy
Metody dostawcy nie mogą mieć żadnych argumentów lub mogą mieć tylko jeden argument Context
.
Wstrzyknięcie zależności
Jeśli do zarządzania zależnościami używasz frameworku do wstrzykiwania zależności, takiego jak Dagger, zalecamy użycie tego frameworku do tworzenia typów w ramach profilu, a następnie wstrzyknięcie tych typów do klasy dostawcy. Metody @CrossProfileProvider
mogą następnie zwracać te wstrzyknięte instancje.
Łącznik profili
Każda konfiguracja profilu krzyżowego musi mieć pojedynczy łącznik profili, który odpowiada za zarządzanie połączeniem z innym profilem.
Domyślny profil połączenia
Jeśli w bazie kodu jest tylko 1 konfiguracja profilu, możesz pominąć tworzenie własnego oprogramowania sprzęgającego i użyć com.google.android.enterprise.connectedapps.CrossProfileConnector
. Jest to wartość domyślna, która jest używana, jeśli nie zostanie określona żadna inna.
Podczas tworzenia łącznika między profilami możesz określić w kreatorze kilka opcji:
Zaplanowana usługa wykonawcza
Jeśli chcesz mieć kontrolę nad wątkami utworzonymi przez pakiet SDK, użyj:
#setScheduledExecutorService()
,Segregator
Jeśli masz konkretne potrzeby dotyczące wiązania profilu, użyj
#setBinder
. Ta funkcja jest prawdopodobnie używana tylko przez kontrolerów zasad urządzenia.
Oprogramowanie sprzęgające niestandardowe profile
Aby móc konfigurować niektóre ustawienia (za pomocą CustomProfileConnector
), musisz mieć niestandardowy profil łącznika. Musisz też mieć go, jeśli potrzebujesz wielu łączników w jednej bazie kodu (np. jeśli masz wiele procesów, zalecamy 1 łącznik na proces).
Podczas tworzenia ProfileConnector
powinien on wyglądać tak:
@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
Aby zmienić nazwę wygenerowanej usługi (na którą należy się powoływać w
AndroidManifest.xml
), użyj elementuserviceClassName=
.primaryProfile
Aby określić profil główny, użyj
primaryProfile
.availabilityRestrictions
Aby zmienić ograniczenia, które pakiet SDK nakłada na połączenia i dostępność profilu, użyj
availabilityRestrictions
.
Kontrolery zasad dotyczących urządzeń
Jeśli Twoja aplikacja jest kontrolerem zasad urządzeń, musisz określić instancję DpcProfileBinder
odwołującą się do DeviceAdminReceiver
.
Jeśli wdrażasz własne oprogramowanie sprzęgające profil:
@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();
}
}
lub użyj domyślnej wartości CrossProfileConnector
:
CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();
Konfiguracja profilu połączonego
Adnotacja @CrossProfileConfiguration
służy do łączenia wszystkich typów profili za pomocą oprogramowania sprzęgającego w celu prawidłowego wysyłania wywołań metod. W tym celu dodaliśmy do klasy adnotację @CrossProfileConfiguration
, która wskazuje na każdego dostawcę. Oto przykład:
@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}
W ten sposób sprawdzisz, czy w przypadku wszystkich typów profili wieloprofilowych mają one ten sam łącznik profilu lub nie mają żadnego łącznika.
serviceSuperclass
Domyślnie wygenerowana usługa będzie używać
android.app.Service
jako superklasy. Jeśli potrzebujesz innej klasy (która musi być podklasą klasyandroid.app.Service
), aby była superklasą, określ ją za pomocą parametruserviceSuperclass=
.serviceClass
Jeśli zostanie podany, usługa nie zostanie wygenerowana. Musi być zgodny z
serviceClassName
w używanym przez Ciebie łączniku profili. Twoja usługa niestandardowa powinna wysyłać wywołania za pomocą wygenerowanej klasy_Dispatcher
w taki sposób:
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;
}
}
Możesz go użyć, jeśli przed lub po wywołaniu między profilami musisz wykonać dodatkowe czynności.
Oprogramowanie sprzęgające
Jeśli używasz innej wtyczki niż domyślna
CrossProfileConnector
, musisz ją określić za pomocą parametruconnector=
.
Widoczność
Każda część aplikacji, która wchodzi w interakcje z usługą Profile Interoperability, musi mieć możliwość wyświetlenia łącznika profilu.
Za pomocą adnotacji @CrossProfileConfiguration
klasa musi mieć możliwość wyświetlania wszystkich dostawców używanych w aplikacji.
Wywołania synchroniczne
Pakiet SDK aplikacji połączonych obsługuje wywołania synchroniczne (blokujące) w przypadkach, gdy są one nieuniknione. Jednak korzystanie z tych wywołań ma też pewne wady (np. możliwość długotrwałego blokowania wywołań), dlatego unikaj ich, jeśli to możliwe. Informacje o wykonywaniu połączeń asynchronicznych znajdziesz w artykule Połączenia asynchroniczne .
Użytkownicy połączenia
Jeśli używasz wywołań synchronicznych, przed wywołaniem wywołań w różnych profilach musisz się upewnić, że zarejestrowany jest element połączenia. W przeciwnym razie zostanie zgłoszony wyjątek. Więcej informacji znajdziesz w sekcji Posiadacze połączenia.
Aby dodać obiekt przechowujący połączenie, wywołaj funkcję ProfileConnector#addConnectionHolder(Object)
z dowolnym obiektem (może to być instancja obiektu, która wywołuje funkcję w ramach profilu). Dzięki temu zostanie zarejestrowane, że ten obiekt korzysta z połączenia i próbuje nawiązać połączenie. Ta metoda musi zostać wywołana przed wykonaniem jakichkolwiek wywołań synchronicznych. Jest to wywołanie nieblokujące, więc może się zdarzyć, że połączenie nie będzie gotowe (lub nie będzie możliwe) w momencie wywołania. W takim przypadku obowiązuje standardowe zachowanie podczas obsługi błędów.
Jeśli podczas wywołania funkcji ProfileConnector#addConnectionHolder(Object)
nie masz odpowiednich uprawnień do działania na różnych profilach lub nie ma profilu, z którym można się połączyć, nie zostanie wygenerowany żaden błąd, ale wywołanie funkcji zwrotnej nie zostanie nigdy wykonane. Jeśli uprawnienia zostaną przyznane później lub inny profil stanie się dostępny, połączenie zostanie nawiązane, a połączenie zwrotne zostanie wywołane.
Zamiast tego możesz użyć metody blokowania ProfileConnector#connect(Object)
, która doda obiekt jako uchwyt połączenia i nawiąże połączenie lub wyrzuci błąd UnavailableProfileException
. Nie można wywołać tej metody z wątku interfejsu użytkownika.
Wywołania funkcji ProfileConnector#connect(Object)
i podobnych ProfileConnector#connect
zwracają obiekty z automatycznym zamykaniem, które automatycznie usuwają element uchwytu po zamknięciu. Dzięki temu możesz m.in.:
try (ProfileConnectionHolder p = connector.connect()) {
// Use the connection
}
Po zakończeniu wykonywania połączeń synchronicznych należy zadzwonić do ProfileConnector#removeConnectionHolder(Object)
. Gdy usuniesz wszystkich właścicieli połączenia, połączenie zostanie zamknięte.
Połączenia
Słuchający na połączenie może być używany do otrzymywania powiadomień o zmianie stanu połączenia, a connector.utils().isConnected
– do określania, czy połączenie jest obecne. Na przykład:
// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
if (crossProfileConnector.utils().isConnected()) {
// Make cross-profile calls.
}
});
Wywołania asynchroniczne
Każda metoda udostępniona w ramach podziału profilu musi być oznaczona jako blokująca (synchroniczna) lub nieblokująca (asynchroniczna). Każda metoda, która zwraca asynchroniczny typ danych (np. ListenableFuture
) lub akceptuje parametr wywołania zwrotnego, jest oznaczona jako nieblokująca. Wszystkie inne metody są oznaczone jako blokujące.
Zalecane są wywołania asynchroniczne. Jeśli musisz używać wywołań synchronicznych, zapoznaj się z artykułem Wywołania synchroniczne.
Wywołania zwrotne
Najprostszym typem wywołania nieblokującego jest metoda void, która jako jeden z parametrów przyjmuje interfejs zawierający metodę do wywołania z wynikiem. Aby te interfejsy działały z pakietem SDK, muszą być opatrzone adnotacjami @CrossProfileCallback
. Na przykład:
@CrossProfileCallback
public interface InstallationCompleteListener {
void installationComplete(int state);
}
Interfejs ten może być używany jako parametr w metodie z adnotacją @CrossProfile
i może być wywoływany w zwykły sposób. Na przykład:
@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
});
Jeśli ten interfejs zawiera jedną metodę, która przyjmuje 0 lub 1 parametry, może być używana również w przypadku wywołań wielu profili naraz.
Za pomocą funkcji wywołania zwrotnego można przekazać dowolną liczbę wartości, ale połączenie będzie otwarte tylko dla pierwszej z nich. Aby dowiedzieć się więcej o trzymaniu połączenia otwartego, aby otrzymywać więcej wartości, zapoznaj się z informacjami na temat właścicieli połączenia.
Metody synchroniczne z wywołaniami zwrotnymi
Jedną z nietypowych funkcji wywołań zwrotnych w pakiecie SDK jest to, że teoretycznie można napisać metodę synchroniczną, która używa wywołania zwrotnego:
public void install(InstallationCompleteListener callback) {
callback.installationComplete(1);
}
W tym przypadku metoda jest w rzeczywistości synchroniczna, mimo wywołania zwrotnego. Ten kod będzie działał prawidłowo:
System.out.println("This prints first");
installer.install(() -> {
System.out.println("This prints second");
});
System.out.println("This prints third");
Jednak gdy wywołasz go za pomocą pakietu SDK, nie będzie on działać w taki sam sposób. Nie ma gwarancji, że metoda instalacji zostanie wywołana przed wydrukowaniem komunikatu „This prints third”. Wszelkie użycia metody oznaczonej przez pakiet SDK jako asynchroniczna nie mogą zakładać, kiedy metoda zostanie wywołana.
proste wywołania zwrotne,
„Proste wywołania zwrotne” to bardziej restrykcyjna forma wywołania zwrotnego, która umożliwia korzystanie z dodatkowych funkcji podczas wywoływania funkcji w różnych profilach. Interfejsy proste muszą zawierać jedną metodę, która może mieć 0 lub 1 parametry.
Aby wymusić, aby interfejs wywołania zwrotnego pozostał, w adnotacji @CrossProfileCallback
podaj wartość simple=true
.
Proste funkcje zwracane po wywołaniu można stosować w różnych metodach, takich jak .both()
, .suppliers()
i inne.
Użytkownicy połączenia
Podczas wywołania asynchronicznego (za pomocą wywołań zwrotnych lub funkcji przyszłych) podczas wywołania zostanie dodany uchwyt połączenia, a usunięty, gdy zostanie przekazane wyjątek lub wartość.
Jeśli oczekujesz, że za pomocą funkcji zwracającej wywołanie zwrotne zostanie przekazanych więcej niż 1 wynik, musisz ręcznie dodać funkcję zwracającą wywołanie zwrotne jako element uchwytujący połączenie:
MyCallback b = //...
connector.addConnectionHolder(b);
profileMyClass.other().registerListener(b);
// Now the connection will be held open indefinitely, once finished:
connector.removeConnectionHolder(b);
Można go też użyć w bloku próby z zasobami:
MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
profileMyClass.other().registerListener(b);
// Other things running while we expect results
}
Jeśli zadzwoniliśmy do Ciebie z prośbą o ponowny kontakt lub w przyszłości, połączenie będzie otwarte do czasu przekazania wyniku. Jeśli stwierdzimy, że wynik nie zostanie przekazany, powinniśmy usunąć funkcję wywołania zwrotnego lub przyszłość jako element uchwytu połączenia:
connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);
Więcej informacji znajdziesz w sekcji Trzymający połączenie.
Transakcje terminowe
Pakiet SDK obsługuje też przyszłe wersje. Jedynym obsługiwanym domyślnie typem przyszłym jest ListenableFuture
, ale można też używać niestandardowych typów przyszłych.
Aby używać funkcji futures, po prostu zadeklaruj obsługiwany typ Future jako typ zwracany metody cross profile, a potem używaj go jak zwykle.
Ma ona tę samą „niezwykłą cechę” co funkcje zwracające przyszłość (np. immediateFuture
), które zachowują się inaczej, gdy są wywoływane na bieżącym profilu niż na innym. Wszelkie użycia metody oznaczonej przez pakiet SDK jako asynchroniczna nie mogą zakładać, kiedy metoda zostanie wywołana.
Wątki
Nie blokuj wyniku funkcji cross-profile future ani funkcji callback na głównym wątku. W niektórych sytuacjach może to spowodować, że kod będzie blokowany przez nieokreślony czas. Dzieje się tak, ponieważ połączenie z innym profilem jest również nawiązywane w głównym wątku, co nigdy nie nastąpi, jeśli jest zablokowane w oczekiwaniu na wynik w wielu profilach.
Dostępność
Odbiorca dostępności może być używany do otrzymywania powiadomień o zmianie stanu dostępności, a connector.utils().isAvailable
do określania, czy inny profil jest dostępny do użycia. Na przykład:
crossProfileConnector.registerAvailabilityListener(() -> {
if (crossProfileConnector.utils().isAvailable()) {
// Show cross-profile content
} else {
// Hide cross-profile content
}
});
Użytkownicy połączenia
Posiadacze połączeń to dowolne obiekty, które są rejestrowane jako obiekty mające interes w utrzymywaniu i utrzymywaniu połączenia między profilami.
Domyślnie podczas wywoływania połączeń asynchronicznych dodawany jest element przechowujący połączenie, gdy rozpoczyna się wywołanie, a usuwany, gdy wystąpi jakikolwiek wynik lub błąd.
Aby uzyskać większą kontrolę nad połączeniem, możesz też ręcznie dodawać i usuwać właścicieli połączenia. Posiadaczy połączenia można dodawać za pomocą connector.addConnectionHolder
, a usuwać za pomocą connector.removeConnectionHolder
.
Gdy dodano co najmniej 1 element przechowujący połączenie, pakiet SDK będzie próbował utrzymać połączenie. Jeśli nie ma żadnych dodanych właścicieli połączenia, można je zamknąć.
Musisz zachować odwołanie do każdego dodanego przez siebie właściciela połączenia i usunąć je, gdy nie będzie już potrzebne.
Wywołania synchroniczne
Przed nawiązywaniem połączeń synchronicznych należy dodać element uchwytujący połączenie. Można to zrobić za pomocą dowolnego obiektu, ale musisz śledzić ten obiekt, aby można go było usunąć, gdy nie będziesz już potrzebować wywołań synchronicznych.
Wywołania asynchroniczne
Podczas wywoływania asynchronicznych wywołań zarządza się automatycznie właścicielami połączeń, aby połączenie było otwarte między wywołaniem a pierwszą odpowiedzią lub błędem. Jeśli połączenie ma przetrwać dłużej (np. aby otrzymywać wiele odpowiedzi za pomocą jednego wywołania zwrotnego), dodaj wywołanie zwrotne jako element przechowujący połączenie i usuń je, gdy nie będzie już potrzebne do otrzymywania kolejnych danych.
Obsługa błędów
Domyślnie wszystkie wywołania do innego profilu, gdy jest on niedostępny, spowodują wyjątek UnavailableProfileException
(lub przekazanie do Future albo wywołanie zwrotne błędu w przypadku wywołania asynchronicznego).
Aby tego uniknąć, deweloperzy mogą użyć funkcji #both()
lub #suppliers()
i napisać kod, który poradzi sobie z dowolną liczbą pozycji na powstałej liście (będzie to 1, jeśli drugi profil jest niedostępny, lub 2, jeśli jest dostępny).
Wyjątki
Wszystkie niesprawdzone wyjątki, które wystąpią po wywołaniu bieżącego profilu, będą propagowane w zwykły sposób. Dotyczy to niezależnie od metody wywołania (#current()
, #personal
, #both
itp.).
Niesprawdzone wyjątki, które występują po wywołaniu innego profilu, spowodują wyjątek ProfileRuntimeException
z pierwotnym wyjątkiem jako przyczyną. Dotyczy to niezależnie od metody wywołania (#other()
, #personal
, #both
itp.).
ifAvailable
Zamiast przechwytywać i obsługiwać wystąpienia UnavailableProfileException
, możesz użyć metody .ifAvailable()
, aby podać wartość domyślną, która zostanie zwrócona zamiast UnavailableProfileException
.
Na przykład:
profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);
Testowanie
Aby umożliwić testowanie kodu, musisz wstrzyknąć instancje swojego łącznika profilu do dowolnego kodu, który go używa (np. do sprawdzania dostępności profilu lub ręcznego łączenia). Powinieneś też wstrzykiwać instancje typów świadomych profilu, w których są one używane.
Udostępniamy fałszywe wersje Twojego złącza i typów, które można wykorzystać w testach.
Najpierw dodaj zależności testu:
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'
Następnie dodaj adnotację @CrossProfileTest
do testowej klasy, aby wskazać klasę z adnotacjami @CrossProfileConfiguration
, którą chcesz przetestować:
@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {
}
Spowoduje to wygenerowanie fałszywych danych dla wszystkich typów i złączy używanych w konfiguracji.
Utwórz w teście instancje tych fałszywych treści:
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();
Skonfiguruj stan profilu:
connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();
Przekaż fałszywy łącznik i klasę profilu krzyżowego do kodu w ramach testu, a potem wywołuj metody.
Połączenia będą przekierowywane do właściwego odbiorcy, a wyjątki będą zgłaszane podczas wykonywania połączeń do odłączonych lub niedostępnych profili.
Uwzględniane rodzaje pyłków
Te typy są obsługiwane bez dodatkowych działań z Twojej strony. Można ich używać jako argumentów lub typów zwracanych we wszystkich wywołaniach między profilami.
- Elementy prymitywne (
byte
,short
,int
,long
,float
,double
,char
,boolean
), - Prosta bryła (
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
,- Wszystko, co implementuje
android.os.Parcelable
, - Wszystko, co implementuje
java.io.Serializable
, - tablice niepierwotne jednowymiarowe,
java.util.Optional
,java.util.Collection
,java.util.List
,java.util.Map
,java.util.Set
,android.util.Pair
,com.google.common.collect.ImmutableMap
.
Parametr typu może zawierać dowolny obsługiwany typ ogólny (np. java.util.Collection
). Na przykład:
java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>>
to prawidłowy typ.
Transakcje terminowe
Te typy są obsługiwane tylko jako typy zwracane:
com.google.common.util.concurrent.ListenableFuture
Niestandardowe opakowania z możliwością zapakowania
Jeśli Twojego typu nie ma na tej liście, zastanów się, czy można go zaimplementować poprawnie za pomocą typu android.os.Parcelable
lub java.io.Serializable
. Jeśli nie widzi opakowań z możliwością zapakowania, aby dodać obsługę Twojego typu.
Niestandardowe kody wrapperów na przyszłość
Jeśli chcesz użyć typu przyszłego, którego nie ma na liście, zapoznaj się z artykułem Opakowania przyszłych funkcji, aby dodać obsługę.
Opakowania do przesyłek
Opakowania do parsowania to sposób, w jaki pakiet SDK dodaje obsługę typów, których nie można parsować, czyli modyfikować. Pakiet SDK zawiera obudowy dla wielu typów, ale jeśli nie ma typu, którego potrzebujesz, musisz napisać własną.
Pakowalny obiekt zastępczy jest klasą przeznaczoną do zastępowania innej klasy i uzyskiwania z niej obiektu zastępczego. Jest on zgodny z określonym kontraktem statycznym i zarejestrowany w SDK, dzięki czemu można go użyć do konwersji danego typu na typ parcelable, a także do wyodrębnienia tego typu z typu parcelable.
Adnotacja
Klasa opakowania pakietu musi być opatrzona adnotacją @CustomParcelableWrapper
, która określa zapakowaną klasę jako originalType
. Na przykład:
@CustomParcelableWrapper(originalType=ImmutableList.class)
Format
Pakowalne opakowania muszą poprawnie implementować Parcelable
i mieć statyczną metodę W of(Bundler, BundlerType, T)
, która otacza owinięty typ, oraz niestatyczną metodę T get()
, która zwraca owinięty typ.
Pakiet SDK będzie używać tych metod, aby zapewnić płynne działanie tego typu.
Bundler
Aby umożliwić opakowanie typów ogólnych (takich jak listy i mapy), metodzie of
przekazywany jest obiekt Bundler
, który może odczytywać (za pomocą #readFromParcel
) i zapisywać (za pomocą #writeToParcel
) wszystkie obsługiwane typy do obiektu Parcel
oraz BundlerType
, który reprezentuje zadeklarowany typ do zapisania.
obie instancje Bundler
i BundlerType
są obiektami parcelowanymi i powinny być zapisane jako część parcelowania opakowania parcelowanego, aby można było go użyć podczas odtwarzania opakowania parcelowanego.
Jeśli BundlerType
reprezentuje typ ogólny, zmienne typu można znaleźć, wywołując .typeArguments()
. Każdy argument typu jest sam w sobie BundlerType
.
Przykład: 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];
}
};
}
Rejestracja w pakiecie SDK
Aby użyć niestandardowego opakowania, musisz je zarejestrować w pakiecie SDK.
Aby to zrobić, określ parcelableWrappers={YourParcelableWrapper.class}
w CustomProfileConnector
lub CrossProfile
w klasie.
Opakowania w przyszłości
Dzięki nim pakiet SDK może dodawać obsługę funkcji futures w różnych profilach. Pakiet SDK domyślnie obsługuje typ ListenableFuture
, ale w przypadku innych typów Future możesz dodać obsługę samodzielnie.
Wrapper dla Future to klasa zaprojektowana do owijania określonego typu Future i umieszczania go w dostępnym dla pakietu SDK miejscu. Jest ona zgodna z zdefiniowanym stałym kontraktem i musi być zarejestrowana w pakiecie SDK.
Adnotacja
Klasa przyszłej obudowy musi być opatrzona adnotacją @CustomFutureWrapper
, która określa obudowaną klasę jako originalType
. Na przykład:
@CustomFutureWrapper(originalType=SettableFuture.class)
Format
Przyszłe opakowania muszą rozszerzać element com.google.android.enterprise.connectedapps.FutureWrapper
.
Przyszłe opakowania muszą mieć stałą metodę W create(Bundler, BundlerType)
, która tworzy wystąpienie opakowania. Jednocześnie powinno to utworzyć instancję zawiniętego typu przyszłego. Powinna być zwracana przez niestatyczne metody T
getFuture()
. Metody onResult(E)
i onException(Throwable)
muszą być zaimplementowane, aby przekazać wynik lub wyjątek do opakowanej funkcji asynchronicznej.
Opakowania w przyszłości muszą też mieć stałą metodę void writeFutureResult(Bundler,
BundlerType, T, FutureResultWriter<E>)
. Powinien on rejestrować wartość przekazywaną w przyszłości w celu uzyskania wyników, a gdy zostanie podany wynik, wywołać funkcję resultWriter.onSuccess(value)
. Jeśli jest to wyjątek, należy wywołać funkcję resultWriter.onFailure(exception)
.
Na koniec opakowania funkcji future muszą też mieć stałą metodę T<Map<Profile, E>>
groupResults(Map<Profile, T<E>> results)
, która przekształca mapę z profilu na przyszłość w mapę z profilu na wynik.
Aby uprościć tę logikę, możesz użyć funkcji CrossProfileCallbackMultiMerger
.
Na przykład:
/** 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;
}
}
Rejestracja w pakiecie SDK
Aby użyć niestandardowego przyszłego opakowania, musisz je zarejestrować w pakiecie SDK.
Aby to zrobić, określ futureWrappers={YourFutureWrapper.class}
w adnotacji CustomProfileConnector
lub CrossProfile
dotyczącej klasy.
Tryb bezpośredniego uruchamiania
Jeśli Twoja aplikacja obsługuje tryb bezpośredniego uruchamiania, przed odblokowaniem profilu może być konieczne wywołanie funkcji na innym profilu. Domyślnie SDK zezwala na połączenia tylko wtedy, gdy drugi profil jest odblokowany.
Aby zmienić to zachowanie, jeśli używasz niestandardowego łącznika profilu, musisz podać:
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();
}
}
Jeśli używasz CrossProfileConnector
, użyj wartości .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT
_AWARE
w kreatorze.
Dzięki tej zmianie będziesz otrzymywać powiadomienia o dostępności i będziesz mieć możliwość nawiązywania połączeń między profilami, gdy inny profil nie jest odblokowany. Twoim obowiązkiem jest zapewnienie, aby połączenia miały dostęp tylko do zaszyfrowanej pamięci urządzenia.