Crea un adattatore di sincronizzazione

Nota: consigliamo di utilizzare WorkManager come soluzione consigliata per la maggior parte dei casi d'uso di elaborazione in background. Fai riferimento alla guida all'elaborazione in background per scoprire la soluzione più adatta alle tue esigenze.

Il componente dell'adattatore di sincronizzazione nella tua app incapsula il codice per le attività che trasferiscono i dati tra il dispositivo e un server. In base alla pianificazione e agli attivatori specificati nell'app, il framework dell'adattatore di sincronizzazione esegue il codice nel componente dell'adattatore di sincronizzazione. Per aggiungere un componente dell'adattatore di sincronizzazione alla tua app, devi aggiungere quanto segue:

Classe adattatore di sincronizzazione.
Una classe che inserisce il codice Data Transfer in un'interfaccia compatibile con il framework dell'adattatore di sincronizzazione.
Associato Service.
Un componente che consente al framework dell'adattatore di sincronizzazione di eseguire il codice nella classe dell'adattatore di sincronizzazione.
File XML dei metadati dell'adattatore di sincronizzazione.
Un file contenente informazioni sull'adattatore di sincronizzazione. Il framework legge questo file per scoprire come caricare e pianificare il trasferimento dei dati.
Dichiarazioni nel file manifest dell'app.
XML che dichiara il servizio associato e punta a sincronizzare i metadati specifici dell'adattatore.

Questa lezione spiega come definire questi elementi.

Crea una classe dell'adattatore di sincronizzazione

In questa parte della lezione imparerai a creare la classe dell'adattatore di sincronizzazione che incapsula il codice Data Transfer. La creazione della classe include l'estensione della classe di base dell'adattatore di sincronizzazione, la definizione dei costruttori per la classe e l'implementazione del metodo in cui definisci le attività di trasferimento dei dati.

Estendi la classe dell'adattatore di sincronizzazione di base

Per creare il componente dell'adattatore di sincronizzazione, inizia estendendo AbstractThreadedSyncAdapter e scrivendo i relativi costruttori. Utilizza i costruttori per eseguire attività di configurazione ogni volta che viene creato il componente dell'adattatore di sincronizzazione da zero, proprio come usi Activity.onCreate() per configurare un'attività. Ad esempio, se la tua app utilizza un fornitore di contenuti per archiviare i dati, utilizza i costruttori per ottenere un'istanza ContentResolver. Poiché nella piattaforma Android versione 3.0 è stata aggiunta una seconda forma del costruttore per supportare l'argomento parallelSyncs, devi crearne due per mantenere la compatibilità.

Nota: il framework dell'adattatore di sincronizzazione è progettato per funzionare con i componenti degli adattatori di sincronizzazione che sono istanze singleton. La creazione di un'istanza del componente dell'adattatore di sincronizzazione è trattata in maggiore dettaglio nella sezione Associazione dell'adattatore di sincronizzazione al framework.

L'esempio seguente mostra come implementare AbstractThreadedSyncAdapter e i relativi costruttori:

Kotlin

/**
 * Handle the transfer of data between a server and an
 * app, using the Android sync adapter framework.
 */
class SyncAdapter @JvmOverloads constructor(
        context: Context,
        autoInitialize: Boolean,
        /**
         * Using a default argument along with @JvmOverloads
         * generates constructor for both method signatures to maintain compatibility
         * with Android 3.0 and later platform versions
         */
        allowParallelSyncs: Boolean = false,
        /*
         * If your app uses a content resolver, get an instance of it
         * from the incoming Context
         */
        val mContentResolver: ContentResolver = context.contentResolver
) : AbstractThreadedSyncAdapter(context, autoInitialize, allowParallelSyncs) {
    ...
}

Java

/**
 * Handle the transfer of data between a server and an
 * app, using the Android sync adapter framework.
 */
public class SyncAdapter extends AbstractThreadedSyncAdapter {
    ...
    // Global variables
    // Define a variable to contain a content resolver instance
    ContentResolver contentResolver;
    /**
     * Set up the sync adapter
     */
    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        /*
         * If your app uses a content resolver, get an instance of it
         * from the incoming Context
         */
        contentResolver = context.getContentResolver();
    }
    ...
    /**
     * Set up the sync adapter. This form of the
     * constructor maintains compatibility with Android 3.0
     * and later platform versions
     */
    public SyncAdapter(
            Context context,
            boolean autoInitialize,
            boolean allowParallelSyncs) {
        super(context, autoInitialize, allowParallelSyncs);
        /*
         * If your app uses a content resolver, get an instance of it
         * from the incoming Context
         */
        contentResolver = context.getContentResolver();
        ...
    }

Aggiungi il codice Data Transfer

Il componente dell'adattatore di sincronizzazione non esegue automaticamente il trasferimento di dati. Incapsula invece il codice Data Transfer in modo che il framework dell'adattatore di sincronizzazione possa eseguire il trasferimento di dati in background, senza il coinvolgimento dell'app. Quando il framework è pronto per sincronizzare i dati dell'applicazione, richiama la tua implementazione del metodo onPerformSync().

Per facilitare il trasferimento dei dati dal codice principale dell'app al componente dell'adattatore di sincronizzazione, il framework dell'adattatore di sincronizzazione chiama onPerformSync() con i seguenti argomenti:

Account
Un oggetto Account associato all'evento che ha attivato l'adattatore di sincronizzazione. Se il server non utilizza account, non è necessario utilizzare le informazioni in questo oggetto.
Extra
Un elemento Bundle contenente i flag inviati dall'evento che ha attivato l'adattatore di sincronizzazione.
Autorità
L'autorità di un fornitore di contenuti nell'ambito del sistema. La tua app deve avere accesso a questo provider. In genere, l'autorità corrisponde a un fornitore di contenuti nella tua app.
Client del fornitore di contenuti
Un ContentProviderClient per il fornitore di contenuti a cui punta l'argomento dell'autorità. ContentProviderClient è un'interfaccia pubblica leggera per un provider di contenuti. Ha le stesse funzionalità di base di ContentResolver. Se utilizzi un fornitore di contenuti per archiviare i dati per la tua app, puoi connetterti al provider con questo oggetto. In caso contrario, puoi ignorarlo.
Risultato della sincronizzazione
Un oggetto SyncResult che utilizzi per inviare informazioni al framework dell'adattatore di sincronizzazione.

Il seguente snippet mostra la struttura generale di onPerformSync():

Kotlin

/*
 * Specify the code you want to run in the sync adapter. The entire
 * sync adapter runs in a background thread, so you don't have to set
 * up your own background processing.
 */
override fun onPerformSync(
        account: Account,
        extras: Bundle,
        authority: String,
        provider: ContentProviderClient,
        syncResult: SyncResult
) {
    /*
     * Put the data transfer code here.
     */
}

Java

/*
 * Specify the code you want to run in the sync adapter. The entire
 * sync adapter runs in a background thread, so you don't have to set
 * up your own background processing.
 */
@Override
public void onPerformSync(
        Account account,
        Bundle extras,
        String authority,
        ContentProviderClient provider,
        SyncResult syncResult) {
    /*
     * Put the data transfer code here.
     */
}

Sebbene l'attuale implementazione di onPerformSync() sia specifica per i requisiti di sincronizzazione dei dati e i protocolli di connessione del server dell'app, esistono alcune attività generali che l'implementazione deve eseguire:

Connessione a un server
Anche se si può presumere che la rete sia disponibile all'avvio del trasferimento di dati, il framework dell'adattatore di sincronizzazione non si connette automaticamente a un server.
Download e caricamento dei dati
Un adattatore di sincronizzazione non automatizza alcuna attività di trasferimento di dati. Se vuoi scaricare i dati da un server e archiviarli in un fornitore di contenuti, devi fornire il codice che richiede i dati, li scarica e li inserisce nel provider. Analogamente, se vuoi inviare dati a un server, devi leggerli da un file, un database o un provider e inviare la richiesta di caricamento necessaria. Devi anche gestire gli errori di rete che si verificano durante il trasferimento dei dati.
Gestione dei conflitti di dati o determinazione del livello di attualità dei dati
Un adattatore di sincronizzazione non gestisce automaticamente i conflitti tra i dati sul server e quelli sul dispositivo. Inoltre, non rileva automaticamente se i dati sul server sono più recenti rispetto a quelli sul dispositivo e viceversa. Devi invece fornire i tuoi algoritmi per gestire questa situazione.
Eseguire la pulizia.
Chiudi sempre le connessioni a un server ed pulisci le cache e i file temporanei al termine del trasferimento dei dati.

Nota:il framework dell'adattatore di sincronizzazione esegue onPerformSync() su un thread in background, quindi non è necessario configurare un'elaborazione in background personalizzata.

Oltre alle attività relative alla sincronizzazione, dovresti provare a combinare le normali attività di rete e aggiungerle a onPerformSync(). Raggruppando tutte le attività di rete in questo metodo, puoi risparmiare la carica della batteria necessaria per avviare e arrestare le interfacce di rete. Per ulteriori informazioni su come rendere più efficiente l'accesso alla rete, consulta il corso di formazione Trasferimento di dati senza svuotare la batteria, che descrive diverse attività di accesso alla rete che puoi includere nel codice di trasferimento dei dati.

Associa l'adattatore di sincronizzazione al framework

Il codice Data Transfer ora è incapsulato in un componente dell'adattatore di sincronizzazione, ma devi fornire al framework l'accesso al tuo codice. A questo scopo, devi creare un elemento Service associato che trasmette un oggetto binder Android speciale dal componente dell'adattatore di sincronizzazione al framework. Con questo oggetto binder, il framework può richiamare il metodo onPerformSync() e trasferirvi i dati.

Crea l'istanza del componente adattatore di sincronizzazione come singleton nel metodo onCreate() del servizio. Creando un'istanza del componente in onCreate(), posticipa la creazione fino all'avvio del servizio, ovvero quando il framework tenta per la prima volta di eseguire il trasferimento di dati. Devi creare un'istanza del componente in modo sicuro per i thread, nel caso in cui il framework dell'adattatore di sincronizzazione accoda più esecuzioni dell'adattatore di sincronizzazione in risposta ai trigger o alla pianificazione.

Ad esempio, lo snippet seguente mostra come creare una classe che implementi l'elemento Service associato, crei un'istanza del componente dell'adattatore di sincronizzazione e recupera l'oggetto binder di Android:

Kotlin

package com.example.android.syncadapter
/**
 * Define a Service that returns an [android.os.IBinder] for the
 * sync adapter class, allowing the sync adapter framework to call
 * onPerformSync().
 */
class SyncService : Service() {
    /*
     * Instantiate the sync adapter object.
     */
    override fun onCreate() {
        /*
         * Create the sync adapter as a singleton.
         * Set the sync adapter as syncable
         * Disallow parallel syncs
         */
        synchronized(sSyncAdapterLock) {
            sSyncAdapter = sSyncAdapter ?: SyncAdapter(applicationContext, true)
        }
    }

    /**
     * Return an object that allows the system to invoke
     * the sync adapter.
     *
     */
    override fun onBind(intent: Intent): IBinder {
        /*
         * Get the object that allows external processes
         * to call onPerformSync(). The object is created
         * in the base class code when the SyncAdapter
         * constructors call super()
         *
         * We should never be in a position where this is called before
         * onCreate() so the exception should never be thrown
         */
        return sSyncAdapter?.syncAdapterBinder ?: throw IllegalStateException()
    }

    companion object {
        // Storage for an instance of the sync adapter
        private var sSyncAdapter: SyncAdapter? = null
        // Object to use as a thread-safe lock
        private val sSyncAdapterLock = Any()
    }
}

Java

package com.example.android.syncadapter;
/**
 * Define a Service that returns an <code><a href="/reference/android/os/IBinder.html">IBinder</a></code> for the
 * sync adapter class, allowing the sync adapter framework to call
 * onPerformSync().
 */
public class SyncService extends Service {
    // Storage for an instance of the sync adapter
    private static SyncAdapter sSyncAdapter = null;
    // Object to use as a thread-safe lock
    private static final Object sSyncAdapterLock = new Object();
    /*
     * Instantiate the sync adapter object.
     */
    @Override
    public void onCreate() {
        /*
         * Create the sync adapter as a singleton.
         * Set the sync adapter as syncable
         * Disallow parallel syncs
         */
        synchronized (sSyncAdapterLock) {
            if (sSyncAdapter == null) {
                sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
            }
        }
    }
    /**
     * Return an object that allows the system to invoke
     * the sync adapter.
     *
     */
    @Override
    public IBinder onBind(Intent intent) {
        /*
         * Get the object that allows external processes
         * to call onPerformSync(). The object is created
         * in the base class code when the SyncAdapter
         * constructors call super()
         */
        return sSyncAdapter.getSyncAdapterBinder();
    }
}

Nota: per un esempio più dettagliato di servizio associato per un adattatore di sincronizzazione, vedi l'app di esempio.

Aggiungi l'account richiesto dal framework

Il framework dell'adattatore di sincronizzazione richiede che ogni adattatore di sincronizzazione abbia un tipo di account. Hai dichiarato il valore del tipo di account nella sezione Aggiungere il file di metadati di Authenticator. Ora devi configurare questo tipo di account nel sistema Android. Per configurare il tipo di account, aggiungi un account segnaposto che utilizzi il tipo di account chiamando addAccountExplicitly().

Il modo migliore per chiamare il metodo è utilizzare il metodo onCreate() dell'attività di apertura dell'app. Il seguente snippet di codice mostra come fare:

Kotlin

...
// Constants
// The authority for the sync adapter's content provider
const val AUTHORITY = "com.example.android.datasync.provider"
// An account type, in the form of a domain name
const val ACCOUNT_TYPE = "example.com"
// The account name
const val ACCOUNT = "placeholderaccount"
...
class MainActivity : FragmentActivity() {

    // Instance fields
    private lateinit var mAccount: Account
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       ...
        // Create the placeholder account
        mAccount = createSyncAccount()
       ...
    }
    ...
    /**
     * Create a new placeholder account for the sync adapter
     */
    private fun createSyncAccount(): Account {
        val accountManager = getSystemService(Context.ACCOUNT_SERVICE) as AccountManager
        return Account(ACCOUNT, ACCOUNT_TYPE).also { newAccount ->
            /*
             * Add the account and account type, no password or user data
             * If successful, return the Account object, otherwise report an error.
             */
            if (accountManager.addAccountExplicitly(newAccount, null, null)) {
                /*
                 * If you don't set android:syncable="true" in
                 * in your <provider> element in the manifest,
                 * then call context.setIsSyncable(account, AUTHORITY, 1)
                 * here.
                 */
            } else {
                /*
                 * The account exists or some other error occurred. Log this, report it,
                 * or handle it internally.
                 */
            }
        }
    }
    ...
}

Java

public class MainActivity extends FragmentActivity {
    ...
    ...
    // Constants
    // The authority for the sync adapter's content provider
    public static final String AUTHORITY = "com.example.android.datasync.provider";
    // An account type, in the form of a domain name
    public static final String ACCOUNT_TYPE = "example.com";
    // The account name
    public static final String ACCOUNT = "placeholderaccount";
    // Instance fields
    Account mAccount;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Create the placeholder account
        mAccount = CreateSyncAccount(this);
        ...
    }
    ...
    /**
     * Create a new placeholder account for the sync adapter
     *
     * @param context The application context
     */
    public static Account CreateSyncAccount(Context context) {
        // Create the account type and default account
        Account newAccount = new Account(
                ACCOUNT, ACCOUNT_TYPE);
        // Get an instance of the Android account manager
        AccountManager accountManager =
                (AccountManager) context.getSystemService(
                        ACCOUNT_SERVICE);
        /*
         * Add the account and account type, no password or user data
         * If successful, return the Account object, otherwise report an error.
         */
        if (accountManager.addAccountExplicitly(newAccount, null, null)) {
            /*
             * If you don't set android:syncable="true" in
             * in your <provider> element in the manifest,
             * then call context.setIsSyncable(account, AUTHORITY, 1)
             * here.
             */
        } else {
            /*
             * The account exists or some other error occurred. Log this, report it,
             * or handle it internally.
             */
        }
    }
    ...
}

Aggiungi il file di metadati dell'adattatore di sincronizzazione

Per collegare il componente dell'adattatore di sincronizzazione al framework, devi fornire nel framework i metadati che descrivono il componente e forniscono flag aggiuntivi. I metadati specificano il tipo di account creato per l'adattatore di sincronizzazione, dichiarano un'autorità del fornitore di contenuti associata alla tua app, controllano una parte dell'interfaccia utente del sistema relativa agli adattatori di sincronizzazione e dichiarano altri flag relativi alla sincronizzazione. Dichiara questi metadati in un file XML speciale archiviato nella directory /res/xml/ del progetto dell'app. Puoi assegnare qualsiasi nome al file, anche se di solito si chiama syncadapter.xml.

Questo file XML contiene un singolo elemento XML <sync-adapter> con i seguenti attributi:

android:contentAuthority
L'autorità URI del tuo fornitore di contenuti. Se hai creato un fornitore di contenuti stub per la tua app nella lezione precedente Creazione di un fornitore di contenuti stub, utilizza il valore specificato per l'attributo android:authorities nell'elemento <provider> che hai aggiunto al file manifest dell'app. Questo attributo è descritto più dettagliatamente nella sezione Dichiarare il provider nel manifest.
Se trasferisci i dati da un fornitore di contenuti a un server con l'adattatore di sincronizzazione, questo valore deve corrispondere all'autorità URI del contenuto che stai utilizzando per i dati. Questo valore è anche una delle autorità specificate nell'attributo android:authorities dell'elemento <provider> che dichiara il tuo provider nel file manifest dell'app.
android:accountType
Il tipo di account richiesto dal framework dell'adattatore di sincronizzazione. Il valore deve essere uguale al valore del tipo di account fornito al momento della creazione del file di metadati dell'autenticatore, come descritto nella sezione Aggiungere il file di metadati di Authenticator. Corrisponde anche al valore specificato per la costante ACCOUNT_TYPE nello snippet di codice riportato nella sezione Aggiungere l'account richiesto dal framework.
Attributi impostazioni
android:userVisible
Imposta la visibilità del tipo di account dell'adattatore di sincronizzazione. Per impostazione predefinita, l'icona dell'account e l'etichetta associati al tipo di account sono visibili nella sezione Account dell'app Impostazioni del sistema, pertanto dovresti rendere invisibile l'adattatore di sincronizzazione, a meno che tu non abbia un tipo di account o un dominio facilmente associato all'app. Se rendi invisibile il tuo tipo di account, puoi comunque consentire agli utenti di controllare l'adattatore di sincronizzazione con un'interfaccia utente in una delle attività dell'app.
android:supportsUploading
Consente di caricare i dati nel cloud. Impostalo su false se la tua app scarica solo dati.
android:allowParallelSyncs
Consente di eseguire contemporaneamente più istanze del componente dell'adattatore di sincronizzazione. Utilizza questa opzione se la tua app supporta più account utente e vuoi consentire a più utenti di trasferire i dati in parallelo. Questo flag non ha effetto se non esegui mai più trasferimenti di dati.
android:isAlwaysSyncable
Indica al framework dell'adattatore di sincronizzazione che può eseguire l'adattatore di sincronizzazione in qualsiasi momento da te specificato. Se vuoi controllare in modo programmatico quando può essere eseguito l'adattatore di sincronizzazione, imposta questo flag su false, quindi chiama requestSync() per eseguire l'adattatore di sincronizzazione. Per scoprire di più sull'esecuzione di un adattatore di sincronizzazione, consulta la lezione Esecuzione di un adattatore di sincronizzazione

L'esempio seguente mostra il codice XML per un adattatore di sincronizzazione che utilizza un singolo account segnaposto ed esegue solo i download.

<?xml version="1.0" encoding="utf-8"?>
<sync-adapter
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:contentAuthority="com.example.android.datasync.provider"
        android:accountType="com.android.example.datasync"
        android:userVisible="false"
        android:supportsUploading="false"
        android:allowParallelSyncs="false"
        android:isAlwaysSyncable="true"/>

Dichiara l'adattatore di sincronizzazione nel file manifest

Dopo aver aggiunto il componente dell'adattatore di sincronizzazione all'app, devi richiedere le autorizzazioni correlate all'utilizzo del componente e dichiarare l'elemento Service associato che hai aggiunto.

Poiché il componente dell'adattatore di sincronizzazione esegue il codice che trasferisce i dati tra la rete e il dispositivo, devi richiedere l'autorizzazione per accedere a Internet. Inoltre, l'app deve richiedere l'autorizzazione per leggere e scrivere le impostazioni dell'adattatore di sincronizzazione, in modo da poter controllare l'adattatore di sincronizzazione in modo programmatico da altri componenti dell'app. Devi anche richiedere un'autorizzazione speciale che consenta all'app di utilizzare il componente di autenticazione creato nella lezione Creazione di un comando Stub Authenticator.

Per richiedere queste autorizzazioni, aggiungi quanto segue al file manifest dell'app come elementi secondari di <manifest>:

android.permission.INTERNET
Consente al codice dell'adattatore di sincronizzazione di accedere a internet in modo da poter scaricare o caricare dati dal dispositivo su un server. Non è necessario aggiungere di nuovo questa autorizzazione se la avevi richiesta in precedenza.
android.permission.READ_SYNC_SETTINGS
Consente all'app di leggere le impostazioni correnti dell'adattatore di sincronizzazione. Ad esempio, devi avere questa autorizzazione per chiamare getIsSyncable().
android.permission.WRITE_SYNC_SETTINGS
Consente all'app di controllare le impostazioni dell'adattatore di sincronizzazione. Devi disporre di questa autorizzazione per impostare esecuzioni periodiche di adattatori di sincronizzazione utilizzando addPeriodicSync(). Questa autorizzazione non è necessaria per chiamare requestSync(). Per ulteriori informazioni sull'esecuzione dell'adattatore di sincronizzazione, consulta la sezione Esecuzione di un adattatore di sincronizzazione.

Lo snippet seguente mostra come aggiungere le autorizzazioni:

<manifest>
...
    <uses-permission
            android:name="android.permission.INTERNET"/>
    <uses-permission
            android:name="android.permission.READ_SYNC_SETTINGS"/>
    <uses-permission
            android:name="android.permission.WRITE_SYNC_SETTINGS"/>
    <uses-permission
            android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
...
</manifest>

Infine, per dichiarare l'elemento Service associato che il framework utilizza per interagire con l'adattatore di sincronizzazione, aggiungi il seguente XML al file manifest dell'app come elemento secondario di <application>:

        <service
                android:name="com.example.android.datasync.SyncService"
                android:exported="false"
                android:process=":sync">
            <intent-filter>
                <action android:name="android.content.SyncAdapter"/>
            </intent-filter>
            <meta-data android:name="android.content.SyncAdapter"
                    android:resource="@xml/syncadapter" />
        </service>

L'elemento <intent-filter> configura un filtro che viene attivato dall'azione intent android.content.SyncAdapter, inviato dal sistema per eseguire l'adattatore di sincronizzazione. Quando viene attivato il filtro, il sistema avvia il servizio associato che hai creato, che in questo esempio è SyncService. L'attributo android:exported="false" consente solo alla tua app e al sistema di accedere a Service. L'attributo android:process=":sync" indica al sistema di eseguire Service in un processo condiviso globale denominato sync. Se nell'app sono presenti più adattatori di sincronizzazione, questi possono condividere questo processo, riducendo così l'overhead.

L'elemento <meta-data> fornisce il nome del file XML dei metadati dell'adattatore di sincronizzazione che hai creato in precedenza. L'attributo android:name indica che questi metadati sono per il framework dell'adattatore di sincronizzazione. L'elemento android:resource specifica il nome del file di metadati.

Ora disponi di tutti i componenti dell'adattatore di sincronizzazione. La lezione successiva illustra come indicare al framework dell'adattatore di sincronizzazione di eseguire l'adattatore di sincronizzazione in risposta a un evento o a cadenza regolare.