Queste sezioni sono pensate come riferimento e non è necessario leggerle dall'alto verso il basso.
Richiedere il consenso dell'utente
Utilizza le API del framework:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
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 | |
---|---|
Attuale | Il profilo attivo su cui stiamo eseguendo l'operazione. |
Altro | (se esistente) Il profilo in cui non viene eseguita l'operazione. |
Personale | Utente 0, il profilo sul dispositivo che non può essere disattivato. |
Lavoro | In genere utente 10, ma può essere superiore, può essere attivato e disattivato, utilizzato per contenere app e dati di lavoro. |
Principale | Facoltativamente definito dall'applicazione. Il profilo che mostra una vista combinata di entrambi i profili. |
Secondario | Se è definito il valore principale, secondario è il profilo che non è principale. |
Fornitore | I 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.
Soluzioni consigliate per l'architettura
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
), utilizzaserviceClassName=
.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 diandroid.app.Service
) come superclasse, specificaserviceSuperclass=
.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 utilizzandoconnector=
.
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.