Erweiterte Themen

Diese Abschnitte dienen nur als Referenz. Sie müssen sie nicht von oben nach unten durchlesen.

Framework-APIs verwenden:

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
AktuellDas aktive Profil, in dem die Ausführung erfolgt.
Sonstiges(falls vorhanden) Das Profil, in dem wir nicht ausführen.
PersönlichNutzer 0, das Profil auf dem Gerät, das nicht deaktiviert werden kann.
GeschäftlichIn 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ärOptional von der Anwendung definiert. Das Profil, das eine zusammengeführte Ansicht der beiden Profile zeigt.
SekundärWenn „primary“ definiert ist, ist „secondary“ das Profil, das nicht primär ist.
AnbieterDie 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.

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 Sie serviceClassName=.

  • 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 von android.app.Service sein muss) als Basisklasse verwendet werden soll, geben Sie serviceSuperclass= 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 mit connector= 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 @CrossProfileCallbackannotiert 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.