Diese Abschnitte dienen nur als Referenz. Sie müssen sie nicht von oben nach unten durchlesen.
Nutzereinwilligung anfordern
Framework-APIs verwenden:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
Diese APIs werden in das SDK eingebunden, um eine einheitlichere API-Oberfläche zu schaffen (z. B. werden UserHandle-Objekte vermieden). Vorerst können Sie sie jedoch direkt aufrufen.
Die Implementierung ist einfach: Wenn Sie interagieren können, fahren Sie fort. Wenn nicht, aber Sie eine Anfrage stellen können, zeigen Sie dem Nutzer eine Aufforderung, ein Banner oder eine Kurzinfo usw. Wenn der Nutzer zustimmt, die Einstellungen aufzurufen, erstellen Sie den Anfrage-Intent und verwenden Sie Context#startActivity
, um den Nutzer dorthin zu senden. Du kannst entweder die Übertragung verwenden, um zu erkennen, wann sich diese Funktion ändert, oder einfach noch einmal prüfen, wenn der Nutzer zurückkommt.
Um dies zu testen, müssen Sie TestDPC in Ihrem Arbeitsprofil öffnen, ganz nach unten scrollen und auswählen, dass der Paketname der Zulassungsliste der verbundenen Apps hinzugefügt werden soll. Dies ahmt die Zulassungsliste für Ihre App durch den Administrator nach.
Glossar
In diesem Abschnitt werden wichtige Begriffe im Zusammenhang mit der plattformübergreifenden Entwicklung definiert.
Profilübergreifende Konfiguration
In einer profilübergreifenden Konfiguration werden ähnliche Anbieterklassen für die profilübergreifenden Funktionen gruppiert und eine allgemeine Konfiguration für die profilübergreifenden Funktionen bereitgestellt.
Normalerweise gibt es eine @CrossProfileConfiguration
-Anmerkung pro Codebasis, in einigen komplexen Anwendungen kann es aber auch mehrere geben.
Profil-Connector
Ein Connector verwaltet Verbindungen zwischen Profilen. Normalerweise verweist jeder Typ des Kreuzprofils auf einen bestimmten Connector. Für jeden profilübergreifenden Typ in einer einzelnen Konfiguration muss derselbe Connector verwendet werden.
Anbieterklasse für profilübergreifende Daten
Eine Anbieterklasse für Profile über mehrere Plattformen hinweg gruppiert verwandte Profile über mehrere Plattformen hinweg.
Mediator
Ein Mediator liegt zwischen dem Hoch- und dem Niedrigst-Level-Code und verteilt Aufrufe an die richtigen Profile und führt die Ergebnisse zusammen. Dies ist der einzige Code, der profilabhängig sein muss. Dies ist ein architektonisches Konzept und nicht im SDK implementiert.
Typ des profilübergreifenden Berichts
Ein profilübergreifender Typ ist eine Klasse oder Schnittstelle mit Methoden, die mit @CrossProfile
annotiert sind. Der Code in diesem Typ muss nicht profilabhängig sein und sollte idealerweise nur auf die lokalen Daten zugreifen.
Profiltypen
Profiltyp | |
---|---|
Aktuell | Das aktive Profil, in dem die Ausführung erfolgt. |
Sonstiges | (falls vorhanden) Das Profil, in dem wir nicht ausführen. |
Persönlich | Nutzer 0, das Profil auf dem Gerät, das nicht deaktiviert werden kann. |
Geschäftlich | In der Regel Nutzer 10, kann aber auch höher sein, kann aktiviert und deaktiviert werden und wird verwendet, um geschäftliche Apps und Daten zu enthalten. |
Primär | Optional von der Anwendung definiert. Das Profil, das eine zusammengeführte Ansicht der beiden Profile zeigt. |
Sekundär | Wenn „primary“ definiert ist, ist „secondary“ das Profil, das nicht primär ist. |
Anbieter | Die Lieferanten für das primäre Profil sind beide Profile, die Lieferanten für das sekundäre Profil sind nur das sekundäre Profil selbst. |
Profil-ID
Eine Klasse, die einen Profiltyp (privat oder beruflich) darstellt. Diese werden von Methoden zurückgegeben, die in mehreren Profilen ausgeführt werden, und können verwendet werden, um in diesen Profilen mehr Code auszuführen. Diese können für eine einfache Speicherung in einem int
serialisiert werden.
Empfohlene Architekturlösungen
In diesem Leitfaden werden empfohlene Strukturen für die Entwicklung effizienter und wartungsfreundlicher profilübergreifender Funktionen in Ihrer Android-App beschrieben.
CrossProfileConnector
in ein Singleton umwandeln
Während des gesamten Lebenszyklus Ihrer Anwendung sollte nur eine Instanz verwendet werden, da sonst parallele Verbindungen erstellt werden. Dies kann entweder mit einem Dependency Injection Framework wie Dagger oder mit einem klassischen Singleton-Muster erfolgen, entweder in einer neuen oder einer vorhandenen Klasse.
Die generierte Profile-Instanz in Ihre Klasse einfügen oder übergeben, wenn Sie den Aufruf ausführen, anstatt sie in der Methode zu erstellen
So können Sie die automatisch generierte FakeProfile
-Instanz später in Ihren Unit-Tests übergeben.
Mediator-Muster
Bei diesem gängigen Muster wird eine Ihrer vorhandenen APIs (z.B. getEvents()
) für alle Aufrufer profilabhängig gemacht. In diesem Fall kann Ihre vorhandene API einfach zu einer „Mediator“-Methode oder -Klasse werden, die den neuen Aufruf des generierten profilübergreifenden Codes enthält.
So müssen nicht alle Aufrufer wissen, wie ein profilübergreifender Aufruf funktioniert, sondern er wird einfach Teil Ihrer API.
Sie können eine Schnittstellenmethode stattdessen als @CrossProfile
annotieren, um zu vermeiden, dass Sie Ihre Implementierungsklassen in einem Anbieter offenlegen müssen.
Das funktioniert gut mit Dependency Injection-Frameworks.
Wenn Sie Daten aus einem profilübergreifenden Aufruf erhalten, sollten Sie überlegen, ein Feld hinzuzufügen, das auf das Profil verweist, aus dem die Daten stammen.
Das kann eine gute Praxis sein, da Sie dies möglicherweise in der UI-Ebene wissen möchten (z.B. ein Abzeichensymbol für geschäftliche Inhalte hinzufügen). Dies kann auch erforderlich sein, wenn Dateneintragsfelder ohne diese Angabe nicht mehr eindeutig sind, z. B. Paketnamen.
Profilübergreifend
In diesem Abschnitt wird beschrieben, wie Sie eigene profilübergreifende Interaktionen erstellen.
Hauptprofile
Die meisten Aufrufe in den Beispielen in diesem Dokument enthalten eine explizite Anleitung dazu, in welchen Profilen sie ausgeführt werden sollen, einschließlich beruflicher, privater und beider Profile.
In der Praxis sollten Sie bei Apps, die nur in einem Profil zusammengeführt werden, diese Entscheidung wahrscheinlich vom Profil abhängig machen, in dem die App ausgeführt wird. Es gibt ähnliche praktische Methoden, die dies ebenfalls berücksichtigen, damit Ihre Codebasis nicht mit If-Else-Profilbedingungen überladen wird.
Beim Erstellen der Connector-Instanz können Sie angeben, welcher Profiltyp „primär“ ist (z.B. „ARBEIT“). Dadurch stehen Ihnen zusätzliche Optionen zur Verfügung, z. B.:
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();
Profiltypen
Klassen und Schnittstellen, die eine mit @CrossProfile
annotierte Methode enthalten, werden als profilübergreifende Typen bezeichnet.
Die Implementierung von Profilen sollte unabhängig vom Profiltyp erfolgen, in dem sie ausgeführt werden. Sie dürfen andere Methoden aufrufen und sollten im Allgemeinen so funktionieren, als würden sie in einem einzelnen Profil ausgeführt. Sie haben nur Zugriff auf die Informationen in ihrem eigenen Profil.
Beispiel für einen profilübergreifenden Typ:
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
Kursanmerkung
Um die leistungsstärkste API bereitzustellen, sollten Sie den Connector für jeden Profiltyp angeben, z. B. so:
@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
Dies ist optional, bedeutet aber, dass die generierte API spezifischer für Typen und strenger bei der Überprüfung zur Laufzeit ist.
Interfaces
Wenn Sie Methoden in einer Benutzeroberfläche mit @CrossProfile
annotieren, geben Sie an, dass es eine Implementierung dieser Methode geben kann, die für alle Profile zugänglich sein sollte.
Sie können eine beliebige Implementierung einer profilübergreifenden Schnittstelle in einem profilübergreifenden Anbieter zurückgeben. Damit geben Sie an, dass diese Implementierung profilübergreifend zugänglich sein soll. Die Implementierungsklassen müssen nicht kommentiert werden.
Anbieter für profilübergreifende Daten
Für jeden Profilübergreifenden Typ muss eine Methode mit der Anmerkung @CrossProfileProvider
angegeben werden. Diese Methoden werden jedes Mal aufgerufen, wenn ein profilübergreifender Aufruf erfolgt. Daher wird empfohlen, für jeden Typ Singletons zu verwenden.
Konstruktor
Ein Anbieter muss einen öffentlichen Konstruktor haben, der entweder keine Argumente oder ein einzelnes Context
-Argument annimmt.
Anbietermethoden
Anbietermethoden dürfen entweder keine oder nur ein einziges Context
-Argument annehmen.
Dependency Injection
Wenn Sie ein Dependency Injection Framework wie Dagger zum Verwalten von Abhängigkeiten verwenden, empfehlen wir, dass Sie Ihre profilübergreifenden Typen wie gewohnt mit diesem Framework erstellen und dann in Ihre Anbieterklasse einschleusen. Die @CrossProfileProvider
-Methoden können dann diese injizierten Instanzen zurückgeben.
Profil-Connector
Jede profilübergreifende Konfiguration muss einen einzelnen Profil-Connector haben, der für die Verwaltung der Verbindung zum anderen Profil verantwortlich ist.
Standardprofil-Connector
Wenn in einer Codebasis nur eine profilübergreifende Konfiguration vorhanden ist, können Sie einen eigenen Profil-Connector erstellen und com.google.android.enterprise.connectedapps.CrossProfileConnector
verwenden. Dieser Wert wird verwendet, wenn keiner angegeben ist.
Beim Erstellen des Cross Profile Connectors können Sie im Builder einige Optionen angeben:
Scheduled Executor Service
Wenn Sie die vom SDK erstellten Threads steuern möchten, verwenden Sie
#setScheduledExecutorService()
.Binder
Wenn Sie spezielle Anforderungen an die Profilbindung haben, verwenden Sie
#setBinder
. Dieser Wert wird wahrscheinlich nur von Device Policy Controllern verwendet.
Benutzerdefinierter Profil-Connector
Sie benötigen einen benutzerdefinierten Profil-Connector, um eine bestimmte Konfiguration (mit CustomProfileConnector
) festlegen zu können. Außerdem ist er erforderlich, wenn Sie mehrere Connectoren in einer einzigen Codebasis benötigen. Wenn Sie beispielsweise mehrere Prozesse haben, empfehlen wir einen Connector pro Prozess.
Wenn Sie eine ProfileConnector
erstellen, sollte sie so aussehen:
@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
Wenn Sie den Namen des generierten Dienstes ändern möchten, auf den in
AndroidManifest.xml
verwiesen werden soll, verwenden SieserviceClassName=
.primaryProfile
Verwenden Sie
primaryProfile
, um das primäre Profil anzugeben.availabilityRestrictions
Mit
availabilityRestrictions
können Sie die Einschränkungen ändern, die das SDK für Verbindungen und die Profilverfügbarkeit vorschreibt.
Device Policy Controller
Wenn Ihre App ein Device Policy Controller ist, müssen Sie eine Instanz von DpcProfileBinder
angeben, die auf Ihre DeviceAdminReceiver
verweist.
Wenn Sie Ihren eigenen Profil-Connector implementieren:
@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();
}
}
oder mit der Standard-CrossProfileConnector
:
CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();
Profilübergreifende Konfiguration
Die @CrossProfileConfiguration
-Anmerkung wird verwendet, um alle profilübergreifenden Typen über einen Connector zu verknüpfen, um Methodenaufrufe korrekt zu senden. Dazu kennzeichnen wir eine Klasse mit @CrossProfileConfiguration
, die auf alle Anbieter verweist, so:
@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}
Dadurch wird geprüft, ob für alle profilübergreifenden Typen entweder derselbe Profil-Connector oder kein Connector angegeben ist.
serviceSuperclass
Standardmäßig verwendet der generierte Dienst
android.app.Service
als Superklasse. Wenn eine andere Klasse (die selbst eine Unterklasse vonandroid.app.Service
sein muss) als Basisklasse verwendet werden soll, geben SieserviceSuperclass=
an.serviceClass
Wenn Sie diesen Parameter angeben, wird kein Dienst generiert. Sie muss mit der
serviceClassName
im verwendeten Profil-Connector übereinstimmen. Ihr benutzerdefinierter Dienst sollte Aufrufe mit der generierten_Dispatcher
-Klasse senden, z. B. so:
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;
}
}
Sie können diese Funktion verwenden, wenn Sie vor oder nach einem profilübergreifenden Aufruf zusätzliche Aktionen ausführen müssen.
Connector
Wenn Sie einen anderen Connector als den Standard-Connector
CrossProfileConnector
verwenden, müssen Sie ihn mitconnector=
angeben.
Sichtbarkeit
Jeder Teil Ihrer Anwendung, der profilübergreifend interagiert, muss Ihren Profil-Connector sehen können.
Die mit @CrossProfileConfiguration
annotierte Klasse muss alle in Ihrer Anwendung verwendeten Anbieter sehen können.
Synchrone Aufrufe
Das Connected Apps SDK unterstützt synchrone (blockierende) Aufrufe, wenn diese unvermeidlich sind. Die Verwendung dieser Aufrufe hat jedoch einige Nachteile, z. B. die Möglichkeit, dass Aufrufe für lange Zeit blockiert werden. Daher wird empfohlen, synchrone Aufrufe nach Möglichkeit zu vermeiden. Informationen zur Verwendung asynchroner Aufrufe finden Sie unter Asynchrone Aufrufe .
Inhaber von Verbindungen
Wenn Sie synchrone Aufrufe verwenden, muss vor Aufrufen zwischen Profilen ein Verbindungsinhaber registriert sein. Andernfalls wird eine Ausnahme ausgelöst. Weitere Informationen finden Sie unter „Inhaber von Verbindungen“.
Wenn Sie einen Kontoinhaber hinzufügen möchten, rufen Sie ProfileConnector#addConnectionHolder(Object)
mit einem beliebigen Objekt auf (z. B. der Objektinstanz, die den profilübergreifenden Aufruf ausführt). Dadurch wird aufgezeichnet, dass dieses Objekt die Verbindung nutzt, und es wird versucht, eine Verbindung herzustellen. Diese Methode muss bevor synchrone Aufrufe erfolgen, aufgerufen werden. Da es sich um einen nicht blockierenden Aufruf handelt, ist es möglich, dass die Verbindung zum Zeitpunkt des Aufrufs noch nicht hergestellt ist (oder nicht hergestellt werden kann). In diesem Fall gilt das übliche Verhalten bei der Fehlerbehandlung.
Wenn du beim Aufrufen von ProfileConnector#addConnectionHolder(Object)
nicht über die entsprechenden plattformübergreifenden Berechtigungen verfügst oder kein Profil für die Verbindung verfügbar ist, wird kein Fehler ausgegeben, aber der verbundene Rückruf wird nie aufgerufen. Wenn die Berechtigung später gewährt wird oder das andere Profil verfügbar wird, wird die Verbindung hergestellt und der Rückruf aufgerufen.
Alternativ ist ProfileConnector#connect(Object)
eine blockierende Methode, bei der das Objekt als Verbindungsinhaber hinzugefügt wird und entweder eine Verbindung hergestellt oder eine UnavailableProfileException
geworfen wird. Diese Methode kann nicht vom
UI-Thread aufgerufen werden.
Aufrufe von ProfileConnector#connect(Object)
und dem ähnlichen ProfileConnector#connect
geben automatisch schließende Objekte zurück, die den Verbindungshalter nach dem Schließen automatisch entfernen. Dies ermöglicht beispielsweise Folgendes:
try (ProfileConnectionHolder p = connector.connect()) {
// Use the connection
}
Wenn Sie mit den synchronen Aufrufen fertig sind, sollten Sie ProfileConnector#removeConnectionHolder(Object)
aufrufen. Sobald alle Inhaber der Verbindung entfernt wurden, wird die Verbindung geschlossen.
Konnektivität
Mit einem Verbindungs-Listener können Sie benachrichtigt werden, wenn sich der Verbindungsstatus ändert. Mit connector.utils().isConnected
können Sie feststellen, ob eine Verbindung besteht. Beispiel:
// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
if (crossProfileConnector.utils().isConnected()) {
// Make cross-profile calls.
}
});
Asynchrone Aufrufe
Jede Methode, die über die Profilgrenze hinweg freigegeben wird, muss als blockierend (synchron) oder nicht blockierend (asynchron) gekennzeichnet sein. Alle Methoden, die einen asynchronen Datentyp zurückgeben (z.B. ListenableFuture
) oder einen Callback-Parameter akzeptieren, werden als nicht blockierend gekennzeichnet. Alle anderen Methoden werden als blockierend gekennzeichnet.
Asynchrone Aufrufe werden empfohlen. Wenn Sie synchrone Aufrufe verwenden müssen, lesen Sie den Hilfeartikel Synchrone Aufrufe.
Callbacks
Die einfachste Art von nicht blockierendem Aufruf ist eine Void-Methode, die als einen ihrer Parameter eine Schnittstelle akzeptiert, die eine Methode enthält, die mit dem Ergebnis aufgerufen werden soll. Damit diese Oberflächen mit dem SDK funktionieren, muss die Oberfläche @CrossProfileCallback
annotiert werden. Beispiel:
@CrossProfileCallback
public interface InstallationCompleteListener {
void installationComplete(int state);
}
Diese Schnittstelle kann dann als Parameter in einer mit @CrossProfile
annotierten Methode verwendet und wie gewohnt aufgerufen werden. Beispiel:
@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
});
Wenn diese Schnittstelle eine einzelne Methode enthält, die entweder null oder einen Parameter annimmt, kann sie auch für Aufrufe mehrerer Profile gleichzeitig verwendet werden.
Über einen Rückruf können beliebig viele Werte übergeben werden, die Verbindung bleibt jedoch nur für den ersten Wert geöffnet. Unter „Verbindungshalter“ finden Sie Informationen dazu, wie Sie die Verbindung offen halten, um weitere Werte zu empfangen.
Synchrone Methoden mit Callbacks
Eine ungewöhnliche Funktion der Verwendung von Callbacks mit dem SDK ist, dass Sie technisch gesehen eine synchrone Methode schreiben können, die einen Callback verwendet:
public void install(InstallationCompleteListener callback) {
callback.installationComplete(1);
}
In diesem Fall ist die Methode trotz des Callbacks tatsächlich synchron. Dieser Code würde korrekt ausgeführt:
System.out.println("This prints first");
installer.install(() -> {
System.out.println("This prints second");
});
System.out.println("This prints third");
Wenn die Funktion jedoch über das SDK aufgerufen wird, verhält sie sich anders. Es gibt keine Garantie dafür, dass die Installationsmethode aufgerufen wurde, bevor „Dieser wird als Dritter ausgegeben“ gedruckt wird. Bei der Verwendung einer vom SDK als asynchron gekennzeichneten Methode dürfen keine Annahmen darüber getroffen werden, wann die Methode aufgerufen wird.
Einfache Callbacks
„Einfache Rückrufe“ sind eine eingeschränktere Form von Rückrufen, die zusätzliche Funktionen bei profilübergreifenden Aufrufen ermöglicht. Einfache Schnittstellen müssen eine einzelne Methode enthalten, die entweder null oder einen Parameter annehmen kann.
Sie können festlegen, dass eine Rückrufschnittstelle erhalten bleiben muss, indem Sie in der @CrossProfileCallback
-Anmerkung simple=true
angeben.
Einfache Rückrufe können mit verschiedenen Methoden wie .both()
und .suppliers()
verwendet werden.
Inhaber von Verbindungen
Wenn Sie einen asynchronen Aufruf ausführen (entweder mit Callbacks oder Futures), wird beim Aufruf ein Verbindungshalter hinzugefügt und entfernt, wenn entweder eine Ausnahme oder ein Wert übergeben wird.
Wenn Sie erwarten, dass über einen Rückruf mehr als ein Ergebnis übergeben wird, sollten Sie den Rückruf manuell als Verbindungshalter hinzufügen:
MyCallback b = //...
connector.addConnectionHolder(b);
profileMyClass.other().registerListener(b);
// Now the connection will be held open indefinitely, once finished:
connector.removeConnectionHolder(b);
Dies kann auch mit einem try-with-resources-Block verwendet werden:
MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
profileMyClass.other().registerListener(b);
// Other things running while we expect results
}
Wenn wir einen Aufruf mit einem Callback oder Future ausführen, bleibt die Verbindung offen, bis ein Ergebnis übergeben wird. Wenn wir feststellen, dass ein Ergebnis nicht übergeben wird, sollten wir den Callback oder Future als Verbindungsinhaber entfernen:
connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);
Weitere Informationen finden Sie unter „Inhaber von Verbindungen“.
Terminkontrakte
Futures werden vom SDK auch nativ unterstützt. Der einzige nativ unterstützte Typ für die Zukunft ist ListenableFuture
. Es können aber auch benutzerdefinierte Typen für die Zukunft verwendet werden.
Wenn Sie Futures verwenden möchten, deklarieren Sie einfach einen unterstützten Future-Typ als Rückgabetyp einer profilübergreifenden Methode und verwenden Sie ihn dann wie gewohnt.
Dies hat dieselbe „ungewöhnliche Funktion“ wie Callbacks: Eine synchrone Methode, die ein Future zurückgibt (z.B. mit immediateFuture
), verhält sich beim Ausführen im aktuellen Profil anders als beim Ausführen in einem anderen Profil. Bei der Verwendung einer vom SDK als asynchron gekennzeichneten Methode dürfen keine Annahmen darüber getroffen werden, wann die Methode aufgerufen wird.
Unterhaltungen
Blockiere das Ergebnis eines profilübergreifenden Futures oder Callbacks nicht im Hauptthread. In einigen Fällen wird Ihr Code dann auf unbestimmte Zeit blockiert. Das liegt daran, dass die Verbindung zum anderen Profil auch über den Hauptthread hergestellt wird. Das ist nie der Fall, wenn er aufgrund eines profilübergreifenden Ergebnisses blockiert ist.
Verfügbarkeit
Mit dem Verfügbarkeits-Listener können Sie benachrichtigt werden, wenn sich der Verfügbarkeitsstatus ändert. Mit connector.utils().isAvailable
können Sie feststellen, ob ein anderes Profil verfügbar ist. Beispiel:
crossProfileConnector.registerAvailabilityListener(() -> {
if (crossProfileConnector.utils().isAvailable()) {
// Show cross-profile content
} else {
// Hide cross-profile content
}
});
Inhaber von Verbindungen
Inhaber von Verbindungen sind beliebige Objekte, die als Inhaber der profilübergreifenden Verbindung registriert sind und ein Interesse daran haben, dass diese Verbindung hergestellt und aufrechterhalten wird.
Bei asynchronen Aufrufen wird standardmäßig ein Verbindungshalter hinzugefügt, wenn der Aufruf gestartet wird, und entfernt, wenn ein Ergebnis oder Fehler auftritt.
Sie können auch manuell hinzugefügt und entfernt werden, um die Verbindung besser zu steuern. Sie können über connector.addConnectionHolder
hinzugefügt und über connector.removeConnectionHolder
entfernt werden.
Wenn mindestens ein Verbindungsinhaber hinzugefügt wurde, versucht das SDK, eine Verbindung aufrechtzuerhalten. Wenn keine Verbindungsinhaber hinzugefügt wurden, kann die Verbindung geschlossen werden.
Sie müssen einen Verweis auf jeden hinzugefügten Kontaktinhaber aufbewahren und ihn entfernen, wenn er nicht mehr relevant ist.
Synchrone Aufrufe
Bevor synchrone Aufrufe erfolgen, sollte ein Verbindungshalter hinzugefügt werden. Dazu kann jedes Objekt verwendet werden. Sie müssen jedoch ein Auge auf dieses Objekt haben, damit es entfernt werden kann, wenn Sie keine synchronen Aufrufe mehr ausführen müssen.
Asynchrone Aufrufe
Bei asynchronen Aufrufen werden die Verbindungshalter automatisch verwaltet, damit die Verbindung zwischen dem Aufruf und der ersten Antwort oder dem ersten Fehler geöffnet ist. Wenn die Verbindung darüber hinaus bestehen bleiben soll (z.B. um mehrere Antworten über einen einzelnen Rückruf zu erhalten), sollten Sie den Rückruf selbst als Verbindungshalter hinzufügen und ihn entfernen, sobald Sie keine weiteren Daten mehr erhalten müssen.
Fehlerbehandlung
Standardmäßig führt jeder Aufruf des anderen Profils, wenn es nicht verfügbar ist, dazu, dass UnavailableProfileException
geworfen (oder an die Future oder den Fehler-Callback für einen asynchronen Aufruf übergeben) wird.
Um dies zu vermeiden, können Entwickler #both()
oder #suppliers()
verwenden und ihren Code so schreiben, dass er mit einer beliebigen Anzahl von Einträgen in der resultierenden Liste umgehen kann. Diese Anzahl ist 1, wenn das andere Profil nicht verfügbar ist, und 2, wenn es verfügbar ist.
Ausnahmen
Alle ungeprüften Ausnahmen, die nach einem Aufruf des aktuellen Profils auftreten, werden wie gewohnt weitergegeben. Das gilt unabhängig von der Methode, mit der der Aufruf erfolgt (#current()
, #personal
, #both
usw.).
Nicht geprüfte Ausnahmen, die nach einem Aufruf des anderen Profils auftreten, führen dazu, dass eine ProfileRuntimeException
mit der ursprünglichen Ausnahme als Ursache ausgelöst wird. Das gilt unabhängig von der Methode, mit der der Aufruf erfolgt (#other()
, #personal
, #both
usw.).
ifAvailable
Anstatt UnavailableProfileException
-Instanzen abzufangen und zu verarbeiten, können Sie mit der Methode .ifAvailable()
einen Standardwert angeben, der zurückgegeben wird, anstatt eine UnavailableProfileException
zu werfen.
Beispiel:
profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);
Test
Damit Ihr Code testbar ist, sollten Sie Instanzen Ihres Profil-Connectors in jeden Code einfügen, in dem er verwendet wird (z. B. um die Profilverfügbarkeit zu prüfen oder eine manuelle Verbindung herzustellen). Außerdem sollten Sie Instanzen Ihrer profilbezogenen Typen dort einfügen, wo sie verwendet werden.
Wir stellen Ihnen Fakes Ihres Anschlusses und Ihrer Typen zur Verfügung, die in Tests verwendet werden können.
Fügen Sie zuerst die Testabhängigkeiten hinzu:
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'
Annotieren Sie dann Ihre Testklasse mit @CrossProfileTest
und geben Sie die zu testende @CrossProfileConfiguration
-annotierte Klasse an:
@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {
}
Dadurch werden Fakes für alle Typen und Anschlüsse generiert, die in der Konfiguration verwendet werden.
Erstellen Sie Instanzen dieser Fakes in Ihrem Test:
private final FakeCrossProfileConnector connector = new
FakeCrossProfileConnector();
private final NotesManager personalNotesManager = new NotesManager(); //
real/mock/fake
private final NotesManager workNotesManager = new NotesManager(); // real/mock/fake
private final FakeProfileNotesManager profileNotesManager =
FakeProfileNotesManager.builder()
.personal(personalNotesManager)
.work(workNotesManager)
.connector(connector)
.build();
Profilstatus einrichten:
connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();
Übergeben Sie den gefälschten Connector und die profilübergreifende Klasse in den zu testenden Code und führen Sie dann Aufrufe aus.
Anrufe werden an das richtige Ziel weitergeleitet und es werden Ausnahmen geworfen, wenn Anrufe an getrennte oder nicht verfügbare Profile gesendet werden.
Unterstützte Arten
Die folgenden Typen werden ohne zusätzlichen Aufwand unterstützt. Sie können als Argumente oder Rückgabetypen für alle profilübergreifenden Aufrufe verwendet werden.
- Primitive (
byte
,short
,int
,long
,float
,double
,char
,boolean
), - Geboxte Primitive (
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
,- Alles, was
android.os.Parcelable
implementiert, - Alles, was
java.io.Serializable
implementiert, - Nicht primitive Arrays mit einer Dimension,
java.util.Optional
,java.util.Collection
java.util.List
java.util.Map
java.util.Set
android.util.Pair
com.google.common.collect.ImmutableMap
.
Für alle unterstützten generischen Typen (z. B. java.util.Collection
) kann jeder unterstützte Typ als Typparameter verwendet werden. Beispiel:
java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>>
ist ein gültiger Typ.
Terminkontrakte
Die folgenden Typen werden nur als Rückgabetypen unterstützt:
com.google.common.util.concurrent.ListenableFuture
Benutzerdefinierte Wrapper für Parcelable
Wenn Ihr Typ nicht in der Liste oben aufgeführt ist, überlegen Sie zuerst, ob er so geändert werden kann, dass entweder android.os.Parcelable
oder java.io.Serializable
korrekt implementiert wird. Wenn dann keine verpackbaren Wrapper gefunden werden, kann keine Unterstützung für Ihren Typ hinzugefügt werden.
Benutzerdefinierte Future-Wrapper
Wenn Sie einen zukünftigen Typ verwenden möchten, der nicht in der Liste oben aufgeführt ist, finden Sie unter Zukünftige Wrapper Informationen dazu, wie Sie Support hinzufügen.
Wrapper für Pakete
Mit Parcelable-Wrappern wird die Unterstützung für nicht paketbare Typen, die nicht geändert werden können, in das SDK aufgenommen. Das SDK enthält Wrapper für viele Typen. Wenn der gewünschte Typ nicht enthalten ist, müssen Sie ihn selbst schreiben.
Eine Parcelable-Wrapper-Klasse ist eine Klasse, die eine andere Klasse umschließt und sie paketierbar macht. Sie folgt einem definierten statischen Vertrag und ist beim SDK registriert. So kann damit ein bestimmter Typ in einen Parcelable-Typ konvertiert und dieser Typ auch aus dem Parcelable-Typ extrahiert werden.
Annotation
Die verpackte Wrapper-Klasse muss mit @CustomParcelableWrapper
annotiert sein. Die verpackte Klasse muss als originalType
angegeben werden. Beispiel:
@CustomParcelableWrapper(originalType=ImmutableList.class)
Format
Wrapper für Parcelable-Objekte müssen Parcelable
korrekt implementieren und eine statische W of(Bundler, BundlerType, T)
-Methode haben, die den verpackten Typ umschließt, sowie eine nicht statische T get()
-Methode, die den verpackten Typ zurückgibt.
Das SDK verwendet diese Methoden, um den Typ nahtlos zu unterstützen.
Bundler
Damit generische Typen (z. B. Listen und Maps) gewickelt werden können, wird der Methode of
ein Bundler
übergeben, mit dem alle unterstützten Typen mithilfe von #readFromParcel
gelesen und mithilfe von #writeToParcel
in eine Parcel
geschrieben werden können, sowie ein BundlerType
, das den angegebenen Typ darstellt, der geschrieben werden soll.
Bundler
- und BundlerType
-Instanzen sind selbst teilbar und sollten im Rahmen der Paketierung des teilbaren Wrappers geschrieben werden, damit sie beim Rekonstruieren des teilbaren Wrappers verwendet werden können.
Wenn BundlerType
einen generischen Typ darstellt, können die Typvariablen durch Aufrufen von .typeArguments()
abgerufen werden. Jedes Typargument ist selbst ein BundlerType
.
Beispiel: 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];
}
};
}
Beim SDK registrieren
Nachdem Sie die benutzerdefinierte Parcelable-Wrapper-Klasse erstellt haben, müssen Sie sie beim SDK registrieren, um sie verwenden zu können.
Geben Sie dazu parcelableWrappers={YourParcelableWrapper.class}
entweder in einer CustomProfileConnector
- oder einer CrossProfile
-Anmerkung zu einem Kurs an.
Future Wrappers
Mithilfe von Future-Wrappern wird im SDK die Unterstützung für Futures für alle Profile hinzugefügt. Das SDK unterstützt standardmäßig ListenableFuture
. Für andere Future-Typen können Sie die Unterstützung selbst hinzufügen.
Ein Future-Wrapper ist eine Klasse, die einen bestimmten Future-Typ umschließt und für das SDK verfügbar macht. Sie folgt einem definierten statischen Vertrag und muss beim SDK registriert werden.
Annotation
Die zukünftige Wrapper-Klasse muss mit @CustomFutureWrapper
annotiert werden. Die gewickelte Klasse muss als originalType
angegeben werden. Beispiel:
@CustomFutureWrapper(originalType=SettableFuture.class)
Format
Künftige Wrapper müssen com.google.android.enterprise.connectedapps.FutureWrapper
erweitern.
Künftige Wrapper müssen eine statische W create(Bundler, BundlerType)
-Methode haben, die eine Instanz des Wrappers erstellt. Gleichzeitig sollte eine Instanz des verpackten Typs „future“ erstellt werden. Dieser Wert sollte von einer nicht statischen T
getFuture()
-Methode zurückgegeben werden. Die Methoden onResult(E)
und onException(Throwable)
müssen implementiert werden, um das Ergebnis oder das throwable an das gewickelte Future zu übergeben.
Future-Wrapper müssen außerdem eine statische void writeFutureResult(Bundler,
BundlerType, T, FutureResultWriter<E>)
-Methode haben. Dies sollte in Zukunft mit den übergebenen Ergebnissen registriert werden. Wenn ein Ergebnis vorliegt, wird resultWriter.onSuccess(value)
aufgerufen. Wenn eine Ausnahme angegeben ist, sollte resultWriter.onFailure(exception)
aufgerufen werden.
Schließlich müssen Future-Wrapper auch eine statische T<Map<Profile, E>>
groupResults(Map<Profile, T<E>> results)
-Methode haben, die eine Zuordnung von Profil zu Zukunft in eine Zukunft einer Zuordnung von Profil zu Ergebnis umwandelt.
Mit CrossProfileCallbackMultiMerger
lässt sich diese Logik vereinfachen.
Beispiel:
/** 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;
}
}
Beim SDK registrieren
Nachdem Sie den benutzerdefinierten Future-Wrapper erstellt haben, müssen Sie ihn beim SDK registrieren, um ihn verwenden zu können.
Geben Sie dazu futureWrappers={YourFutureWrapper.class}
entweder in einer CustomProfileConnector
- oder einer CrossProfile
-Anmerkung zu einem Kurs an.
Direkter Boot-Modus
Wenn Ihre App den Direktstartmodus unterstützt, müssen Sie möglicherweise profilübergreifende Aufrufe ausführen, bevor das Profil entsperrt wird. Standardmäßig erlaubt das SDK nur Verbindungen, wenn das andere Profil entsperrt ist.
Wenn Sie dieses Verhalten ändern möchten und einen benutzerdefinierten Profil-Connector verwenden, sollten Sie Folgendes angeben: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();
}
}
Wenn Sie CrossProfileConnector
verwenden, verwenden Sie .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT
_AWARE
für den Builder.
Durch diese Änderung werden Sie über die Verfügbarkeit informiert und können Profile übergreifend anrufen, auch wenn das andere Profil nicht entsperrt ist. Sie sind dafür verantwortlich, dass bei Anrufen nur auf den verschlüsselten Gerätespeicher zugegriffen wird.