Argomenti avanzati

Queste sezioni sono pensate come riferimento e non è necessario leggerle dall'alto verso il basso.

Utilizza le API del framework:

Queste API verranno inserite nell'SDK per una piattaforma API più coerente (ad es. evitando gli oggetti UserHandle), ma per il momento puoi chiamarle direttamente.

L'implementazione è semplice: se puoi interagire, procedi. In caso contrario, ma puoi richiedere, mostra all'utente un prompt/banner/una descrizione comando/ecc. Se l'utente accetta di andare alle Impostazioni, crea l'intent di richiesta e utilizza Context#startActivity per indirizzarlo lì. Puoi utilizzare la trasmissione per rilevare quando questa funzionalità cambia oppure semplicemente ricontrollare quando l'utente ritorna.

Per verificare, devi aprire TestDPC nel tuo profilo di lavoro, andare in fondo e selezionare l'aggiunta del nome del pacchetto alla lista consentita delle app collegate. Questo simula l 'inserimento della tua app nella lista consentita da parte dell'amministratore.

Glossario

Questa sezione definisce i termini chiave relativi allo sviluppo cross-profile.

Configurazione tra i profili

Una configurazione di profili tra più account raggruppa Classi di provider di profili tra più account correlati e fornisce la configurazione generale per le funzionalità di questo tipo di profili. In genere, esiste un'annotazione @CrossProfileConfiguration per codebase, ma in alcune applicazioni complesse potrebbero essercene diverse.

Connettore dei profili

Un connettore gestisce i collegamenti tra i profili. In genere, ogni tipo di profilo tra più account fa riferimento a un connettore specifico. Ogni tipo di profilo incrociato in una singola configurazione deve utilizzare lo stesso connettore.

Classe del provider di più profili

Una classe di provider di profili incrociati raggruppa tipi di profili incrociati correlati.

Mediator

Un mediatore si trova tra il codice di alto livello e quello di basso livello, distribuendo le chiamate ai profili corretti e unendo i risultati. Questo è l'unico codice che deve essere consapevole del profilo. Si tratta di un concetto di architettura piuttosto che di un elemento integrato nell'SDK.

Tipo di profilo incrociato

Un tipo di profilo incrociato è una classe o un'interfaccia contenente metodi annotati@CrossProfile. Il codice di questo tipo non deve essere consapevole del profilo e idealmente dovrebbe agire solo sui dati locali.

Tipi di profilo

Tipo profilo
AttualeIl profilo attivo su cui stiamo eseguendo l'operazione.
Altro(se esistente) Il profilo in cui non viene eseguita l'operazione.
PersonaleUtente 0, il profilo sul dispositivo che non può essere disattivato.
LavoroIn genere utente 10, ma può essere superiore, può essere attivato e disattivato, utilizzato per contenere app e dati di lavoro.
PrincipaleFacoltativamente definito dall'applicazione. Il profilo che mostra una vista combinata di entrambi i profili.
SecondarioSe è definito il valore principale, secondario è il profilo che non è principale.
FornitoreI fornitori del profilo principale sono entrambi i profili, mentre i fornitori del profilo secondario sono solo il profilo secondario stesso.

Identificatore profilo

Una classe che rappresenta un tipo di profilo (personale o di lavoro). Questi valori verranno restituito da metodi che vengono eseguiti su più profili e possono essere utilizzati per eseguire altro codice su questi profili. Questi possono essere serializzati in un int per un comodo immagazzinamento.

Questa guida illustra le strutture consigliate per creare funzionalità cross-profile efficienti e manutenibili all'interno della tua app per Android.

Converti CrossProfileConnector in un singleton

È necessario utilizzare una sola istanza durante tutto il ciclo di vita dell'applicazione, altrimenti verranno create connessioni parallele. Questo può essere fatto utilizzando un framework di Dependency Injection come Dagger o un classico pattern Singleton, in una nuova classe o in una esistente.

Inserisci o passa l'istanza di Profile generata nel tuo corso per quando effettui la chiamata, anziché crearla nel metodo

In questo modo, potrai passare l'istanza FakeProfile generata automaticamente nei test di unità in un secondo momento.

Prendi in considerazione il pattern Mediatore

Questo pattern comune consiste nel rendere consapevole del profilo una delle tue API esistenti (ad es. getEvents()) per tutti i relativi chiamanti. In questo caso, l'API esistente può semplicemente diventare un metodo o una classe "mediatore" che contiene la nuova chiamata al codice generato per più profili.

In questo modo, non devi obbligare ogni utente che effettua una chiamata a conoscere la chiamata tra profili, poiché diventa parte della tua API.

Valuta la possibilità di annotare un metodo dell'interfaccia come @CrossProfile per evitare di dover esporre le classi di implementazione in un provider

Questo approccio funziona bene con i framework di Dependency Injection.

Se ricevi dati da una chiamata tra profili, valuta la possibilità di aggiungere un campo che rimandi al profilo di provenienza

Questa può essere una buona prassi, poiché potresti volerlo sapere a livello di livello dell'interfaccia utente (ad es. aggiungendo un'icona di badge ai contenuti di lavoro). Potrebbe essere necessario anche se gli identificatori dei dati non sono più univoci senza, ad esempio i nomi dei pacchetti.

Profilo tra più dispositivi

Questa sezione illustra come creare le tue interazioni tra profili.

Profili principali

La maggior parte delle chiamate negli esempi di questo documento contiene istruzioni esplicite su su quali profili eseguire l'operazione, inclusi quelli di lavoro, personali e entrambi.

In pratica, per le app con un'esperienza unita in un solo profilo, probabilmente vorrai che questa decisione dipenda dal profilo su cui stai eseguendo l'app, quindi esistono metodi simili e pratici che prendono in considerazione anche questo aspetto, per evitare che la base di codice sia disseminata di condizioni di profilo if-else.

Quando crei l'istanza del connettore, puoi specificare quale tipo di profilo è il tuo "principale" (ad es. "LAVORO"). In questo modo vengono attivate opzioni aggiuntive, ad esempio:

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();

Tipi di profili incrociati

Le classi e le interfacce che contengono un metodo annotato @CrossProfile sono chiamate tipi di profili incrociati.

L'implementazione dei tipi di profili tra più profili deve essere indipendente dal profilo su cui vengono eseguiti. Possono effettuare chiamate ad altri metodi e in genere dovrebbero funzionare come se fossero eseguiti su un singolo profilo. Potranno accedere allo stato solo nel proprio profilo.

Un esempio di tipo di profilo incrociato:

public class Calculator {
  @CrossProfile
  public int add(int a, int b) {
    return a + b;
  }
}

Annotazione del corso

Per fornire l'API più efficace, devi specificare il connettore per ogni tipo di profilo tra più profili, come segue:

@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
  @CrossProfile
  public int add(int a, int b) {
    return a + b;
  }
}

Questo passaggio è facoltativo, ma significa che l'API generata sarà più specifica per i tipi e più rigorosa per il controllo in fase di compilazione.

Interfacce

Se annoti i metodi di un'interfaccia come @CrossProfile, dichiari che puoi essere presente un'implementazione di questo metodo che deve essere accessibile in tutti i profili.

Puoi restituire qualsiasi implementazione di un'interfaccia cross-profile in un provider cross-profile e, in questo modo, dichiari che questa implementazione deve essere accessibile tra più profili. Non è necessario annotare le classi di implementazione.

Fornitori di più profili

Ogni Cross Profile Type deve essere fornito da un metodo annotato @CrossProfileProvider. Questi metodi verranno chiamati ogni volta che viene eseguita una chiamata tra profili, pertanto ti consigliamo di gestire oggetti singoli per ogni tipo.

Costruttore

Un provider deve avere un costruttore pubblico che non accetta argomenti o un singolo argomento Context.

Metodi del provider

I metodi del provider non devono accettare alcun argomento o un singolo argomento Context.

Iniezione di dipendenze

Se utilizzi un framework di Dependency Injection come Dagger per gestire le dipendenze, ti consigliamo di utilizzare questo framework per creare i tipi di profili tra più profili come faresti di solito e poi iniettarli nella classe del provider. I metodi @CrossProfileProvider possono quindi restituire queste istanze iniettate.

Connettore dei profili

Ogni configurazione tra profili deve avere un unico connettore di profili, responsabile della gestione della connessione all'altro profilo.

Connettore profilo predefinito

Se in una base di codice è presente una sola configurazione tra profili, puoi evitare di creare il tuo connettore di profili e utilizzarecom.google.android.enterprise.connectedapps.CrossProfileConnector. Si tratta del valore predefinito utilizzato se non viene specificato alcun valore.

Quando crei il connettore tra profili, puoi specificare alcune opzioni nel generatore:

  • Scheduled Executor Service

    Se vuoi avere il controllo sui thread creati dall'SDK, utilizza #setScheduledExecutorService().

  • Raccoglitore

    Se hai esigenze specifiche in merito al collegamento del profilo, utilizza #setBinder. Probabilmente viene utilizzato solo dai controller Device Policy.

Connettore profilo personalizzato

Per poter impostare alcune configurazioni (utilizzando CustomProfileConnector), devi avere un connettore di profilo personalizzato e ne devi avere uno se hai bisogno di più connettori in un'unica base di codice (ad esempio, se hai più processi, consigliamo un connettore per processo).

Quando crei un ProfileConnector, il risultato dovrebbe essere simile a questo:

@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

    Per modificare il nome del servizio generato (a cui deve essere fatto riferimento nel AndroidManifest.xml), utilizza serviceClassName=.

  • primaryProfile

    Per specificare il profilo principale, utilizza primaryProfile.

  • availabilityRestrictions

    Per modificare le restrizioni imposte dall'SDK alle connessioni e alla disponibilità del profilo, utilizza availabilityRestrictions.

Controller dei criteri dei dispositivi

Se la tua app è un controller dei criteri dei dispositivi, devi specificare un'istanza di DpcProfileBinder che fa riferimento a DeviceAdminReceiver.

Se stai implementando il tuo connettore del profilo:

@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();
  }
}

o utilizzando il valore predefinito CrossProfileConnector:

CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();

Configurazione tra i profili

L'annotazione @CrossProfileConfiguration viene utilizzata per collegare tutti i tipi di cross profilo utilizzando un connettore al fine di inviare correttamente le chiamate ai metodi. Per farlo, annottiamo una classe con @CrossProfileConfiguration che rimanda a ogni fornitore, come segue:

@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}

In questo modo verrà verificato che per tutti i tipi di profili tra più profili sia specificato lo stesso connettore del profilo o nessun connettore.

  • serviceSuperclass

    Per impostazione predefinita, il servizio generato utilizzerà android.app.Service come superclasse. Se hai bisogno di una classe diversa (che deve essere a sua volta una sottoclasse di android.app.Service) come superclasse, specifica serviceSuperclass=.

  • serviceClass

    Se specificato, non verrà generato alcun servizio. Deve corrispondere al valore serviceClassName nel connettore del profilo in uso. Il servizio personalizzato deve inviare le chiamate utilizzando la classe _Dispatcher generata come segue:

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;
  }
}

Questa opzione può essere utilizzata se devi eseguire azioni aggiuntive prima o dopo una chiamata tra profili.

  • Connettore

    Se utilizzi un connettore diverso da CrossProfileConnector predefinito, devi specificarlo utilizzando connector=.

Visibilità

Ogni parte dell'applicazione che interagisce tra profili deve essere in grado di vedere il tuo connettore di profili.

La classe annotata @CrossProfileConfiguration deve essere in grado di vedere ogni fornitore utilizzato nella tua applicazione.

Chiamate sincrone

L'SDK Connected Apps supporta le chiamate sincrone (bloccanti) nei casi in cui non sono evitabili. Tuttavia, l'utilizzo di queste chiamate presenta una serie di svantaggi (ad esempio, il potenziale blocco delle chiamate per lunghi periodi di tempo), pertanto è consigliabile evitare le chiamate sincrone, se possibile. Per utilizzare le chiamate asincrone, consulta Chiamate asincrone .

Titolari di connessione

Se utilizzi chiamate sincrone, devi assicurarti che sia registrato un detentore della connessione prima di effettuare chiamate tra profili, altrimenti verrà generata un'eccezione. Per saperne di più, consulta Titolari delle connessioni.

Per aggiungere un proprietario della connessione, chiama ProfileConnector#addConnectionHolder(Object) con qualsiasi oggetto (potenzialmente l'istanza dell'oggetto che effettua la chiamata tra profili). Verrà registrato che questo oggetto utilizza la connessione e tenterà di effettuare una connessione. Deve essere chiamato prima di effettuare chiamate sincrone. Si tratta di una chiamata non bloccante, pertanto è possibile che la connessione non sia pronta (o potrebbe non essere possibile) al momento della chiamata. In questo caso, si applica il normale comportamento di gestione degli errori.

Se non disponi delle autorizzazioni appropriate per più profili quando chiami ProfileConnector#addConnectionHolder(Object) o se non è disponibile alcun profilo da collegare, non verrà generato alcun errore, ma il callback collegato non verrà mai chiamato. Se l'autorizzazione viene concessa in un secondo momento o se l'altro profilo diventa disponibile, la connessione verrà stabilita e verrà chiamato il callback.

In alternativa, ProfileConnector#connect(Object) è un metodo di blocco che aggiunge l'oggetto come gestore della connessione ed esegue una connessione o genera un UnavailableProfileException. Questo metodo non può essere chiamato dal thread dell'interfaccia utente.

Le chiamate a ProfileConnector#connect(Object) e simili ProfileConnector#connect restituiscono oggetti con chiusura automatica che rimuovereranno automaticamente il gestore della connessione una volta chiusi. Ciò consente utilizzi quali:

try (ProfileConnectionHolder p = connector.connect()) {
  // Use the connection
}

Al termine delle chiamate sincrone, devi chiamare ProfileConnector#removeConnectionHolder(Object). Una volta rimossi tutti i titolari della connessione, la connessione verrà chiusa.

Connettività

Un ascoltatore di connessione può essere utilizzato per ricevere una notifica quando lo stato della connessione cambia e connector.utils().isConnected può essere utilizzato per determinare se è presente una connessione. Ad esempio:

// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
  if (crossProfileConnector.utils().isConnected()) {
    // Make cross-profile calls.
  }
});

Chiamate asincrone

Ogni metodo esposto nella suddivisione del profilo deve essere designato come bloccante (sincrono) o non bloccante (asincrono). Qualsiasi metodo che restituisce un tipo di dati asincroni (ad es. un ListenableFuture) o accetta un parametro callback è contrassegnato come non bloccante. Tutti gli altri metodi sono contrassegnati come bloccanti.

Sono consigliate le chiamate asincrone. Se devi utilizzare chiamate sincrone, consulta Chiamate sincrone.

Callback

Il tipo più semplice di chiamata non bloccante è un metodo void che accetta come uno tra i suoi parametri un'interfaccia contenente un metodo da chiamare con il risultato. Affinché queste interfacce funzionino con l'SDK, devono essere annotate @CrossProfileCallback. Ad esempio:

@CrossProfileCallback
public interface InstallationCompleteListener {
  void installationComplete(int state);
}

Questa interfaccia può essere utilizzata come parametro in un metodo @CrossProfile annotato e chiamata come di consueto. Ad esempio:

@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
});

Se questa interfaccia contiene un singolo metodo che accetta zero o un parametro, può essere utilizzata anche nelle chiamate a più profili contemporaneamente.

È possibile passare un numero qualsiasi di valori utilizzando un callback, ma la connessione verrà mantenuta aperta solo per il primo valore. Consulta i titolari della connessione per informazioni su come mantenere aperta la connessione per ricevere più valori.

Metodi sincroni con callback

Una funzionalità insolita dell'utilizzo dei callback con l'SDK è che tecnicamente potresti scrivere un metodo sincrono che utilizza un callback:

public void install(InstallationCompleteListener callback) {
  callback.installationComplete(1);
}

In questo caso, il metodo è effettivamente sincrono, nonostante il callback. Questo codice verrà eseguito correttamente:

System.out.println("This prints first");
installer.install(() -> {
        System.out.println("This prints second");
});
System.out.println("This prints third");

Tuttavia, se viene chiamato utilizzando l'SDK, il comportamento non sarà lo stesso. Non c'è alcuna garanzia che il metodo di installazione sia stato chiamato prima che venga stampato "Questo stampa il terzo". Eventuali utilizzi di un metodo contrassegnato come asincrono dall'SDK non devono fare supposizioni su quando verrà chiamato il metodo.

Callback semplici

I "richiami semplici" sono una forma di richiamo più restrittiva che consente di usufruire di funzionalità aggiuntive quando si effettuano chiamate tra profili. Le interfacce semplici devono contenere un singolo metodo, che può accettare zero o un parametro.

Puoi imporre che un'interfaccia di callback debba rimanere specificando simple=true nell'annotazione @CrossProfileCallback.

I callback semplici sono utilizzabili con vari metodi, come .both(), .suppliers() e altri.

Titolari di connessione

Quando viene effettuata una chiamata asincrona (utilizzando callback o future), viene aggiunto un detentore della connessione durante la chiamata e rimosso quando viene passata un'eccezione o un valore.

Se prevedi che venga passato più di un risultato utilizzando un callback, devi aggiungere manualmente il callback come gestore della connessione:

MyCallback b = //...
connector.addConnectionHolder(b);

  profileMyClass.other().registerListener(b);

  // Now the connection will be held open indefinitely, once finished:
  connector.removeConnectionHolder(b);

Può essere utilizzato anche con un blocco try-with-resources:

MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
  profileMyClass.other().registerListener(b);

  // Other things running while we expect results
}

Se effettuiamo una chiamata con un callback o un futuro, la connessione verrà mantenuta aperta fino a quando non viene passato un risultato. Se stabiliamo che un risultato non verrà passato, dobbiamo rimuovere il callback o il futuro come gestore della connessione:

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

Per saperne di più, consulta Titolari delle connessioni.

Futures

I futures sono supportati anche in modo nativo dall'SDK. L'unico tipo Future supportato in modo nativo è ListenableFuture, anche se è possibile utilizzare tipi Future personalizzati. Per utilizzare gli oggetti Future, è sufficiente dichiarare un tipo Future supportato come tipo restituito di un metodo tra profili e utilizzarlo normalmente.

Ha la stessa "funzionalità insolita" dei callback, in cui un metodo sincrono che restituisce un futuro (ad es. utilizzando immediateFuture) si comporta in modo diverso se viene eseguito nel profilo corrente rispetto a un altro profilo. Eventuali utilizzi di un metodo contrassegnato come asincrono dall'SDK non devono fare supposizioni su quando verrà chiamato il metodo.

Thread

Non bloccare il risultato di un futuro o di un callback cross-profile nel thread principale. In questo caso, in alcuni casi il codice verrà bloccato indefinitamente. Questo accade perché anche il collegamento all'altro profilo viene stabilito sul thread principale, il che non si verifica mai se è bloccato in attesa di un risultato tra profili.

Disponibilità

L'ascoltatore di disponibilità può essere utilizzato per ricevere una notifica quando lo stato della disponibilità cambia e connector.utils().isAvailable può essere utilizzato per determinare se è disponibile un altro profilo. Ad esempio:

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

Titolari di connessione

I titolari della connessione sono oggetti arbitrari che vengono registrati come interessati alla creazione e al mantenimento della connessione tra profili.

Per impostazione predefinita, quando effettui chiamate asincrone, un gestore della connessione viene aggiunto all'inizio della chiamata e rimosso quando si verifica un risultato o un errore.

I titolari della connessione possono essere aggiunti e rimossi anche manualmente per esercitare un maggiore controllo sulla connessione. I titolari dei collegamenti possono essere aggiunti utilizzando connector.addConnectionHolder e rimossi utilizzando connector.removeConnectionHolder.

Se è stato aggiunto almeno un proprietario della connessione, l'SDK tenterà di mantenere una connessione. Se non sono stati aggiunti titolari della connessione, la connessione può essere chiusa.

Devi mantenere un riferimento a qualsiasi proprietario di collegamento che aggiungi e rimuoverlo quando non è più pertinente.

Chiamate sincrone

Prima di effettuare chiamate sincrone, è necessario aggiungere un proprietario della connessione. Questa operazione può essere eseguita utilizzando qualsiasi oggetto, ma devi tenere traccia dell'oggetto in modo che possa essere rimosso quando non è più necessario effettuare chiamate sincrone.

Chiamate asincrone

Quando effettui chiamate asincrone, i gestori delle connessioni verranno gestiti automaticamente in modo che la connessione sia aperta tra la chiamata e la prima risposta o l'errore. Se vuoi che la connessione rimanga attiva anche dopo (ad es. per ricevere più risposte utilizzando un singolo callback), devi aggiungere il callback stesso come gestore della connessione e rimuoverlo quando non hai più bisogno di ricevere ulteriori dati.

Gestione degli errori

Per impostazione predefinita, tutte le chiamate all'altro profilo quando non è disponibile causeranno l'emissione di un UnavailableProfileException (o la sua impostazione nel Future o nel callback di errore per una chiamata asincrona).

Per evitare questo problema, gli sviluppatori possono utilizzare #both() o #suppliers() e scrivere il codice per gestire un numero qualsiasi di voci nell'elenco risultante (1 se l'altro profilo non è disponibile o 2 se è disponibile).

Eccezioni

Eventuali eccezioni non selezionate che si verificano dopo una chiamata al profilo corrente verranno propagate come di consueto. Questo vale indipendentemente dal metodo utilizzato per effettuare la chiamata (#current(), #personal, #both e così via).

Le eccezioni non verificate che si verificano dopo una chiamata all'altro profilo causeranno un ProfileRuntimeException con l'eccezione originale come causa. Questo vale indipendentemente dal metodo utilizzato per effettuare la chiamata (#other(), #personal, #both e così via).

ifAvailable

In alternativa a intercettare e gestire le istanze UnavailableProfileException, puoi utilizzare il metodo .ifAvailable() per fornire un valore predefinito che verrà restituito anziché generare un UnavailableProfileException.

Ad esempio:

profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);

Test

Per rendere il codice testabile, devi iniettare istanze del connettore del profilo in qualsiasi codice che lo utilizza (per verificare la disponibilità del profilo, per eseguire il collegamento manualmente e così via). Dovresti anche iniettare istanze dei tuoi tipi aware del profilo dove vengono utilizzati.

Forniamo repliche del connettore e dei tipi che possono essere utilizzati nei test.

Innanzitutto, aggiungi le dipendenze di test:

  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'

Quindi, annota la classe di test con @CrossProfileTest, identificando la @CrossProfileConfiguration classe annotata da testare:

@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {

}

Ciò comporterà la generazione di falsi per tutti i tipi e connettori utilizzati nella configurazione.

Crea istanze di questi falsi nel 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();

Configura lo stato del profilo:

connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();

Passa il connettore falso e la classe di profilo tra più account al codice in test, quindi effettua le chiamate.

Le chiamate verranno instradate al target corretto e verranno lanciate eccezioni quando vengono effettuate chiamate a profili disconnessi o non disponibili.

Tipi supportati

I seguenti tipi sono supportati senza alcun intervento da parte tua. Questi possono essere utilizzati come argomenti o tipi di ritorno per tutte le chiamate tra profili.

  • Elementi primitivi (byte, short, int, long, float, double, char, boolean),
  • Elementi primitivi con riquadro (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,
  • Qualsiasi cosa che implementi android.os.Parcelable,
  • Qualsiasi cosa che implementi java.io.Serializable,
  • Array non primitivi a dimensione singola
  • java.util.Optional,
  • java.util.Collection,
  • java.util.List,
  • java.util.Map,
  • java.util.Set,
  • android.util.Pair,
  • com.google.common.collect.ImmutableMap.

Qualsiasi tipo generico supportato (ad esempio java.util.Collection) può avere qualsiasi tipo supportato come parametro di tipo. Ad esempio:

java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>> è un tipo valido.

Futures

I seguenti tipi sono supportati solo come tipi di ritorno:

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

Wrapper Parcelable personalizzati

Se il tuo tipo non è presente nell'elenco precedente, valuta innanzitutto se è possibile implementare correttamente android.os.Parcelable o java.io.Serializable. Se non riesce a vedere i wrapper aggregabili, non può aggiungere il supporto per il tuo tipo.

Wrapper Future personalizzati

Se vuoi utilizzare un tipo futuro non presente nell'elenco precedente, consulta future wrappers per aggiungere il supporto.

Wrapper parcellable

I wrapper Parcelable consentono all'SDK di aggiungere il supporto per i tipi non parcellabili che non possono essere modificati. L'SDK include wrapper per molti tipi, ma se il tipo che devi utilizzare non è incluso, devi scriverne uno.

Un wrapper Parcelable è una classe progettata per avvolgere un'altra classe e renderla parcellabile. Segue un contratto statico definito ed è registrato nell'SDK, pertanto può essere utilizzato per convertire un determinato tipo in un tipo parcellabile ed estrarlo anche dal tipo parcellabile.

Annotazione

La classe del wrapper parcelable deve essere annotata @CustomParcelableWrapper, specificando la classe avvolta come originalType. Ad esempio:

@CustomParcelableWrapper(originalType=ImmutableList.class)

Formato

I wrapper Parcelable devono implementare Parcelable correttamente e devono avere un metodo W of(Bundler, BundlerType, T) statico che avvolge il tipo avvolto e un metodo T get() non statico che restituisce il tipo avvolto.

L'SDK utilizzerà questi metodi per fornire un supporto completo per il tipo.

Bundler

Per consentire l'incapsulamento di tipi generici (ad esempio elenchi e mappe), al metodo of viene passato un Bundler in grado di leggere (utilizzando #readFromParcel) e scrivere (utilizzando #writeToParcel) tutti i tipi supportati in un Parcel e un BundlerType che rappresenta il tipo dichiarato da scrivere.

Le istanze Bundler e BundlerType sono a loro volta parcellabili e devono essere scritte nell'ambito del parcellamento del wrapper parcellabile, in modo che possano essere utilizzate durante la ricostruzione del wrapper parcellabile.

Se BundlerType rappresenta un tipo generico, le variabili di tipo possono essere trovate chiamando .typeArguments(). Ogni argomento di tipo è a sua volta un BundlerType.

Per un esempio, vedi 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];
      }
    };
}

Registrati all'SDK

Una volta creato, per utilizzare il wrapper parcelable personalizzato dovrai registrarlo con l'SDK.

A tal fine, specifica parcelableWrappers={YourParcelableWrapper.class} in un'annotazione CustomProfileConnector o CrossProfile su un corso.

Future Wrappers

I Future Wrapper sono il modo in cui l'SDK aggiunge il supporto per i futures in tutti i profili. L'SDK include il supporto per ListenableFuture per impostazione predefinita, ma puoi aggiungere il supporto per altri tipi di Future.

Un Future Wrapper è una classe progettata per avvolgere un tipo Future specifico e renderlo disponibile per l'SDK. Segue un contratto statico definito e deve essere registrato nell'SDK.

Annotazione

La classe wrapper futura deve essere annotata @CustomFutureWrapper, specificando la classe con wrapping come originalType. Ad esempio:

@CustomFutureWrapper(originalType=SettableFuture.class)

Formato

I wrapper futuri devono estendere com.google.android.enterprise.connectedapps.FutureWrapper.

I wrapper futuri devono avere un metodo W create(Bundler, BundlerType) statico che crei un'istanza del wrapper. Allo stesso tempo, dovrebbe essere creata un'istanza del tipo futuro con wrapping. Deve essere restituito da un metodo T getFuture() non statico. I metodi onResult(E) e onException(Throwable) devono essere implementati per passare il risultato o l'oggetto throwable al futuro con wrapping.

I wrapper futuri devono avere anche un metodo void writeFutureResult(Bundler, BundlerType, T, FutureResultWriter<E>) statico. Questo dovrebbe essere registrato con il valore passato in futuro per i risultati e, quando viene fornito un risultato, chiama resultWriter.onSuccess(value). Se viene fornita un'eccezione, deve essere chiamata resultWriter.onFailure(exception).

Infine, i wrapper futuri devono avere anche un metodo T<Map<Profile, E>> groupResults(Map<Profile, T<E>> results) statico che converta una mappa da profilo a futuro in un futuro di una mappa da profilo a risultato. CrossProfileCallbackMultiMerger può essere utilizzato per semplificare questa logica.

Ad esempio:

/** 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;
  }
}

Registrati all'SDK

Una volta creato, per utilizzare il wrapper futuro personalizzato dovrai registrarlo con l'SDK.

A tal fine, specifica futureWrappers={YourFutureWrapper.class} in un'annotazione CustomProfileConnector o CrossProfile in un corso.

Modalità di avvio diretto

Se la tua app supporta la modalità di avvio diretto, potrebbe essere necessario effettuare chiamate tra profili prima che il profilo venga sbloccato. Per impostazione predefinita, l'SDK consente le connessioni solo quando l'altro profilo è sbloccato.

Per modificare questo comportamento, se utilizzi un connettore di profili personalizzati, devi specificare 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();
  }
}

Se utilizzi CrossProfileConnector, usa .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT _AWARE nel strumento per la creazione.

Con questa modifica, riceverai una notifica della disponibilità e potrai effettuare chiamate tra profili quando l'altro profilo non è sbloccato. È tua responsabilità assicurarti che le chiamate accedano solo allo spazio di archiviazione criptato del dispositivo.