Criar um adaptador de sincronização

Observação:recomendamos o WorkManager como a solução para a maioria dos casos de uso de processamento em segundo plano. Consulte o guia de processamento em segundo plano para saber qual solução funciona melhor para você.

O componente do adaptador de sincronização no app encapsula o código das tarefas que transferem dados entre o dispositivo e um servidor. Com base na programação e nos gatilhos fornecidos no app, o framework do adaptador de sincronização executa o código no componente do adaptador de sincronização. Para adicionar um componente do adaptador de sincronização ao app, é preciso adicionar o seguinte:

Classe do adaptador de sincronização.
Uma classe que une o código de transferência de dados em uma interface compatível com o framework do adaptador de sincronização.
Service vinculado.
Um componente que permite que o framework do adaptador de sincronização execute o código na sua classe do adaptador de sincronização.
Arquivo de metadados XML do adaptador de sincronização.
Um arquivo contendo informações sobre o adaptador de sincronização. O framework lê esse arquivo para descobrir como carregar e programar a transferência de dados.
Declarações no manifesto do app.
XML que declara o serviço vinculado e aponta para metadados específicos do adaptador de sincronização.

Esta lição mostra como definir esses elementos.

Criar uma classe de adaptador de sincronização

Nesta parte da lição, você aprenderá a criar a classe do adaptador de sincronização que encapsula o código de transferência de dados. A criação da classe inclui estender a classe base do adaptador de sincronização, definir construtores para a classe e implementar o método em que você define as tarefas de transferência de dados.

Estender a classe base do adaptador de sincronização

Para criar o componente do adaptador de sincronização, comece estendendo AbstractThreadedSyncAdapter e criando os construtores dele. Use os construtores para executar tarefas de configuração sempre que o componente do adaptador de sincronização for criado do zero, da mesma forma que você usa Activity.onCreate() para configurar uma atividade. Por exemplo, se o app usa um provedor de conteúdo para armazenar dados, use os construtores para receber uma instância de ContentResolver. Como uma segunda forma do construtor foi adicionada à versão 3.0 da plataforma Android para oferecer suporte ao argumento parallelSyncs, você precisa criar duas formas do construtor para manter a compatibilidade.

Observação:o framework do adaptador de sincronização foi projetado para funcionar com componentes do adaptador de sincronização que são instâncias únicas. A instanciação do componente do adaptador de sincronização é abordada com mais detalhes na seção Vincular o adaptador de sincronização ao framework.

O exemplo abaixo mostra como implementar AbstractThreadedSyncAdapter e os construtores dele.

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

Adicionar o código de transferência de dados

O componente do adaptador de sincronização não faz a transferência de dados automaticamente. Em vez disso, ele encapsula o código de transferência de dados para que o framework do adaptador de sincronização possa executar a transferência de dados em segundo plano, sem o envolvimento do app. Quando o framework está pronto para sincronizar os dados do aplicativo, ele invoca a implementação do método onPerformSync().

Para facilitar a transferência de dados do código do app principal para o componente do adaptador de sincronização, o framework do adaptador de sincronização chama onPerformSync() com os seguintes argumentos:

Conta
Um objeto Account associado ao evento que acionou o adaptador de sincronização. Se o servidor não usa contas, você não precisa usar as informações nesse objeto.
Extras
Um Bundle contendo sinalizações enviadas pelo evento que acionou o adaptador de sincronização.
Autoridade
A autoridade de um provedor de conteúdo no sistema. Seu app precisa ter acesso a esse provedor. Normalmente, a autoridade corresponde a um provedor de conteúdo no seu app.
Cliente do provedor de conteúdo
Um ContentProviderClient do provedor de conteúdo apontado pelo argumento de autoridade. O ContentProviderClient é uma interface pública leve para um provedor de conteúdo. Ele tem a mesma funcionalidade básica de um ContentResolver. Se você estiver usando um provedor de conteúdo para armazenar dados do seu app, poderá se conectar ao provedor com esse objeto. Caso contrário, ignore-o.
Resultado da sincronização
Um objeto SyncResult usado para enviar informações para o framework do adaptador de sincronização.

O snippet a seguir mostra a estrutura geral de 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.
     */
}

Embora a implementação real de onPerformSync() seja específica para os requisitos de sincronização de dados do app e protocolos de conexão do servidor, sua implementação precisa executar algumas tarefas gerais:

Conectar-se a um servidor
Embora você possa presumir que a rede esteja disponível quando a transferência de dados for iniciada, o framework do adaptador de sincronização não se conecta automaticamente a um servidor.
Fazer o download e upload de dados
Um adaptador de sincronização não automatiza tarefas de transferência de dados. Se você quiser fazer o download de dados de um servidor e armazená-los em um provedor de conteúdo, forneça o código que solicita esses dados, faça o download deles e os insira no provedor. Da mesma forma, se você quiser enviar dados a um servidor, será preciso lê-los de um arquivo, banco de dados ou provedor e enviar a solicitação de upload necessária. Também é preciso lidar com erros de rede que ocorrem enquanto a transferência de dados está em execução.
Gerenciar conflitos de dados ou determinar a atualidade dos dados
Um adaptador de sincronização não gerencia automaticamente conflitos entre dados no servidor e no dispositivo. Além disso, ele não detecta automaticamente se os dados no servidor são mais recentes do que os dados no dispositivo ou vice-versa. Em vez disso, é necessário fornecer seus próprios algoritmos para lidar com essa situação.
Limpeza
Sempre encerre as conexões com um servidor e limpe arquivos temporários e armazenados em cache ao final da transferência de dados.

Observação:o framework do adaptador de sincronização executa onPerformSync() em uma linha de execução em segundo plano. Assim, você não precisa configurar seu processamento em segundo plano.

Além das tarefas relacionadas à sincronização, tente combinar as tarefas regulares relacionadas à rede e adicioná-las a onPerformSync(). Ao concentrar todas as tarefas de rede nesse método, você economiza a energia da bateria necessária para iniciar e interromper as interfaces de rede. Para saber mais sobre como tornar o acesso à rede mais eficiente, consulte a aula de treinamento Como transferir dados sem consumo de bateria, que descreve várias tarefas de acesso à rede que você pode incluir no código de transferência de dados.

Vincular o adaptador de sincronização ao framework

Agora você tem o código de transferência de dados encapsulado em um componente do adaptador de sincronização, mas precisa fornecer ao framework acesso ao código. Para fazer isso, você precisa criar um Service vinculado que transmita um objeto binder especial do Android do componente do adaptador de sincronização para o framework. Com esse objeto binder, o framework pode invocar o método onPerformSync() e transmitir dados para ele.

Instancie o componente do adaptador de sincronização como um singleton no método onCreate() do serviço. Ao instanciar o componente em onCreate(), você adia a criação até que o serviço seja iniciado, o que acontece quando o framework tenta executar a transferência de dados pela primeira vez. Você precisa instanciar o componente de maneira segura para as linhas de execução, caso o framework do adaptador de sincronização enfileire várias execuções em resposta a gatilhos ou em programação.

Por exemplo, o snippet a seguir mostra como criar uma classe que implementa o Service vinculado, instancia o componente do adaptador de sincronização e recebe o objeto binder do 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();
    }
}

Observação:para ver um exemplo mais detalhado de um serviço vinculado a um adaptador de sincronização, consulte o app de exemplo.

Adicionar a conta exigida pelo framework

O framework do adaptador de sincronização exige que cada adaptador de sincronização tenha um tipo de conta. Você declarou o valor do tipo de conta na seção Adicionar o arquivo de metadados do Authenticator. Agora você precisa configurar esse tipo de conta no sistema Android. Para configurar o tipo de conta, adicione uma conta de marcador que use o tipo chamando addAccountExplicitly().

O melhor lugar para chamar o método é no método onCreate() da atividade de abertura do app. O snippet de código a seguir mostra como fazer isso.

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

Adicionar o arquivo de metadados do adaptador de sincronização

Para conectar o componente do adaptador de sincronização ao framework, é necessário fornecer ao framework metadados que descrevam o componente e forneçam outras sinalizações. Os metadados especificam o tipo de conta que você criou para o adaptador de sincronização, declara uma autoridade do provedor de conteúdo associada ao app, controla uma parte da interface do usuário do sistema relacionada aos adaptadores de sincronização e declara outras sinalizações relacionadas à sincronização. Declare esses metadados em um arquivo XML especial armazenado no diretório /res/xml/ no projeto do app. Você pode dar qualquer nome ao arquivo, embora ele seja normalmente chamado de syncadapter.xml.

Esse arquivo XML contém um único elemento XML <sync-adapter> que tem os seguintes atributos:

android:contentAuthority
A autoridade de URI para seu provedor de conteúdo. Se você criou um provedor de conteúdo stub para seu app na lição anterior Como criar um provedor de conteúdo stub, use o valor especificado para o atributo android:authorities no elemento <provider> adicionado ao manifesto do app. Esse atributo é descrito com mais detalhes na seção Declarar o provedor no manifesto.
Se você transferir dados de um provedor de conteúdo para um servidor com o adaptador de sincronização, esse valor precisará ser o mesmo da autoridade de URI de conteúdo usada para esses dados. Esse valor também é uma das autoridades que você especifica no atributo android:authorities do elemento <provider> que declara seu provedor no manifesto do app.
android:accountType
O tipo de conta exigido pelo framework do adaptador de sincronização. O valor precisa ser o mesmo que o valor do tipo de conta fornecido ao criar o arquivo de metadados do autenticador, conforme descrito na seção Adicionar o arquivo de metadados do autenticador. É também o valor especificado para a constante ACCOUNT_TYPE no snippet de código da seção Adicionar a conta exigida pelo framework.
Atributos de configuração
android:userVisible
Define a visibilidade do tipo de conta do adaptador de sincronização. Por padrão, o ícone da conta e o rótulo associados ao tipo de conta ficam visíveis na seção Contas do app Configurações do sistema. Portanto, torne o adaptador de sincronização invisível, a menos que você tenha um tipo de conta ou domínio facilmente associado ao seu app. Se você tornar o tipo de conta invisível, ainda será possível permitir que os usuários controlem o adaptador de sincronização com uma interface do usuário em uma das atividades do app.
android:supportsUploading
Permite fazer upload de dados para a nuvem. Defina como false se o app só fizer o download de dados.
android:allowParallelSyncs
Permite que várias instâncias do componente do adaptador de sincronização sejam executadas ao mesmo tempo. Use se o app for compatível com várias contas de usuário e você quiser permitir que vários usuários transfiram dados em paralelo. Essa sinalização não terá efeito se você nunca executar várias transferências de dados.
android:isAlwaysSyncable
Indica ao framework do adaptador de sincronização que ele pode ser executado a qualquer momento especificado. Se você quiser controlar programaticamente quando o adaptador de sincronização pode ser executado, defina essa sinalização como false e, em seguida, chame requestSync() para executá-lo. Para saber mais sobre como executar um adaptador de sincronização, consulte a lição Executar um adaptador de sincronização

O exemplo a seguir mostra o XML de um adaptador de sincronização que usa uma única conta de marcador de posição e só faz downloads.

<?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"/>

Declarar o adaptador de sincronização no manifesto

Depois de adicionar o componente do adaptador de sincronização ao app, você precisa solicitar permissões relacionadas ao uso do componente e declarar o Service vinculado que foi adicionado.

Como o componente do adaptador de sincronização executa um código que transfere dados entre a rede e o dispositivo, é necessário solicitar permissão para acessar a Internet. Além disso, seu app precisa solicitar permissão para ler e gravar as configurações do adaptador de sincronização. Assim, você pode controlá-lo programaticamente a partir de outros componentes no app. Você também precisa solicitar uma permissão especial que permita que o app use o componente do autenticador criado na lição Como criar um stub de autenticação.

Para solicitar essas permissões, adicione o seguinte ao manifesto do app como elementos filhos de <manifest>:

android.permission.INTERNET
Permite que o código do adaptador de sincronização acesse a Internet para fazer o download ou upload de dados do dispositivo para um servidor. Não é necessário adicionar essa permissão novamente se você a solicitou anteriormente.
android.permission.READ_SYNC_SETTINGS
Permite que o app leia as configurações atuais do adaptador de sincronização. Por exemplo, você precisa dessa permissão para chamar getIsSyncable().
android.permission.WRITE_SYNC_SETTINGS
Permite que o app controle as configurações do adaptador de sincronização. Você precisa dessa permissão para definir a execução periódica do adaptador de sincronização usando addPeriodicSync(). Essa permissão não é necessária para chamar requestSync(). Para saber mais sobre como executar o adaptador de sincronização, consulte Como executar um adaptador de sincronização.

O snippet a seguir mostra como adicionar as permissões.

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

Por fim, para declarar o Service vinculado que o framework usa para interagir com o adaptador de sincronização, adicione o seguinte XML ao manifesto do app como um elemento filho de <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>

O elemento <intent-filter> configura um filtro que é acionado pela ação da intent android.content.SyncAdapter, enviado pelo sistema para executar o adaptador de sincronização. Quando o filtro é acionado, o sistema inicia o serviço vinculado que você criou, que neste exemplo é SyncService. O atributo android:exported="false" permite que apenas seu app e o sistema acessem o Service. O atributo android:process=":sync" instrui o sistema a executar a Service em um processo compartilhado global chamado sync. Se você tiver vários adaptadores de sincronização no app, eles poderão compartilhar esse processo, o que reduz a sobrecarga.

O elemento <meta-data> fornece o nome do arquivo XML de metadados do adaptador de sincronização criado anteriormente. O atributo android:name indica que esses metadados são para o framework do adaptador de sincronização. O elemento android:resource especifica o nome do arquivo de metadados.

Agora você tem todos os componentes para seu adaptador de sincronização. A próxima lição mostra como instruir o framework do adaptador de sincronização a executá-lo em resposta a um evento ou em uma programação regular.