Запуск адаптера синхронизации

Примечание. Для большинства фоновых процессов мы рекомендуем использовать WorkManager. Чтобы узнать, какое решение лучше всего подходит для вас, ознакомьтесь с руководством по фоновой обработке.

На предыдущих уроках этого курса вы узнали, как создать компонент адаптера синхронизации, который инкапсулирует код передачи данных, и как добавить дополнительные компоненты, позволяющие подключить адаптер синхронизации к системе. Теперь у вас есть все необходимое для установки приложения, которое включает адаптер синхронизации, но ни один из фрагментов кода, которые вы видели, на самом деле не запускает адаптер синхронизации.

Попробуйте запустить адаптер синхронизации по расписанию или как косвенный результат какого-либо события. Например, вы можете настроить запуск адаптера синхронизации по регулярному расписанию, либо по истечении определенного периода времени, либо в определенное время дня. Вы также можете запустить адаптер синхронизации, когда в данных, хранящихся на устройстве, произойдут изменения. Не рекомендуется запускать адаптер синхронизации в результате действия пользователя, поскольку в этом случае вы не сможете в полной мере воспользоваться возможностями планирования, которые предоставляет платформа адаптера синхронизации. Например, не стоит добавлять в пользовательский интерфейс кнопку обновления.

Вы можете запустить адаптер синхронизации следующими способами:

При изменении данных на сервере
Запускать адаптер синхронизации в ответ на сообщение от сервера, указывающее, что данные на сервере изменились. Этот параметр позволяет обновлять данные с сервера на устройстве без снижения производительности и сокращения времени работы от батареи.
При изменении данных устройства
Запускать адаптер синхронизации при изменении данных на устройстве. Этот вариант позволяет отправлять измененные данные с устройства на сервер и особенно полезен, если вам нужно, чтобы на сервере всегда были актуальные данные с устройства. Этот вариант легко реализовать, если вы действительно храните данные в поставщике контента. Если вы используете заглушку поставщика контента, обнаружить изменения данных может быть сложнее.
Через регулярные интервалы
Запускайте адаптер синхронизации по истечении выбранного интервала или в определенное время каждый день.
По запросу
Запустите адаптер синхронизации в ответ на действие пользователя. Однако для удобства пользователей лучше использовать один из автоматизированных вариантов. Используя автоматические настройки, вы экономите заряд батареи и сетевые ресурсы.

Далее в этом уроке мы подробно рассмотрим каждый из вариантов.

Запускать адаптер синхронизации при изменении данных на сервере

Если ваше приложение передает данные с сервера и данные на сервере часто меняются, вы можете использовать адаптер синхронизации для скачивания в ответ на изменения данных. Чтобы запустить адаптер синхронизации, сервер должен отправить специальное сообщение в BroadcastReceiver вашего приложения. В ответ на это сообщение вызовите ContentResolver.requestSync(), чтобы сообщить фреймворку адаптера синхронизации о необходимости запустить адаптер синхронизации.

Google Cloud Messaging (GCM) предоставляет компоненты для сервера и устройства, необходимые для работы этой системы обмена сообщениями. Использование GCM для запуска передачи данных более надежно и эффективно, чем опрос серверов для получения статуса. Для опроса требуется Service, который всегда активен, а GCM использует BroadcastReceiver, который активируется при получении сообщения. При регулярном опросе сервера заряд батареи расходуется, даже если обновлений нет. GCM же отправляет сообщения только по мере необходимости.

Примечание. Если вы используете GCM для запуска адаптера синхронизации через широковещательную рассылку на все устройства, на которых установлено ваше приложение, помните, что они получают ваше сообщение примерно в одно и то же время. Это может привести к одновременному запуску нескольких экземпляров адаптера синхронизации, что вызовет перегрузку сервера и сети. Чтобы избежать такой ситуации при широковещательной рассылке на все устройства, можно отложить запуск адаптера синхронизации на период, уникальный для каждого устройства.

В приведенном ниже фрагменте кода показано, как запустить requestSync() в ответ на входящее сообщение GCM.

Kotlin

...
// Constants
// Content provider authority
const val AUTHORITY = "com.example.android.datasync.provider"
// Account type
const val ACCOUNT_TYPE = "com.example.android.datasync"
// Account
const val ACCOUNT = "default_account"
// Incoming Intent key for extended data
const val KEY_SYNC_REQUEST = "com.example.android.datasync.KEY_SYNC_REQUEST"
...
class GcmBroadcastReceiver : BroadcastReceiver() {
    ...
    override fun onReceive(context: Context, intent: Intent) {
        // Get a GCM object instance
        val gcm: GoogleCloudMessaging = GoogleCloudMessaging.getInstance(context)
        // Get the type of GCM message
        val messageType: String? = gcm.getMessageType(intent)
        /*
         * Test the message type and examine the message contents.
         * Since GCM is a general-purpose messaging system, you
         * may receive normal messages that don't require a sync
         * adapter run.
         * The following code tests for a a boolean flag indicating
         * that the message is requesting a transfer from the device.
         */
        if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE == messageType
            && intent.getBooleanExtra(KEY_SYNC_REQUEST, false)) {
            /*
             * Signal the framework to run your sync adapter. Assume that
             * app initialization has already created the account.
             */
            ContentResolver.requestSync(mAccount, AUTHORITY, null)
            ...
        }
        ...
    }
    ...
}

Java

public class GcmBroadcastReceiver extends BroadcastReceiver {
    ...
    // Constants
    // Content provider authority
    public static final String AUTHORITY = "com.example.android.datasync.provider";
    // Account type
    public static final String ACCOUNT_TYPE = "com.example.android.datasync";
    // Account
    public static final String ACCOUNT = "default_account";
    // Incoming Intent key for extended data
    public static final String KEY_SYNC_REQUEST =
            "com.example.android.datasync.KEY_SYNC_REQUEST";
    ...
    @Override
    public void onReceive(Context context, Intent intent) {
        // Get a GCM object instance
        GoogleCloudMessaging gcm =
                GoogleCloudMessaging.getInstance(context);
        // Get the type of GCM message
        String messageType = gcm.getMessageType(intent);
        /*
         * Test the message type and examine the message contents.
         * Since GCM is a general-purpose messaging system, you
         * may receive normal messages that don't require a sync
         * adapter run.
         * The following code tests for a a boolean flag indicating
         * that the message is requesting a transfer from the device.
         */
        if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)
            &&
            intent.getBooleanExtra(KEY_SYNC_REQUEST)) {
            /*
             * Signal the framework to run your sync adapter. Assume that
             * app initialization has already created the account.
             */
            ContentResolver.requestSync(mAccount, AUTHORITY, null);
            ...
        }
        ...
    }
    ...
}

Запуск адаптера синхронизации при изменении данных поставщика контента

Если ваше приложение собирает данные в поставщике контента и вы хотите обновлять сервер при каждом обновлении поставщика, вы можете настроить приложение на автоматический запуск адаптера синхронизации. Для этого зарегистрируйте наблюдателя для поставщика контента. Когда данные в поставщике контента меняются, фреймворк поставщика контента вызывает наблюдателя. В наблюдателе вызовите метод requestSync(), чтобы сообщить фреймворку о необходимости запустить адаптер синхронизации.

Примечание. Если вы используете заглушку поставщика контента, у вас нет данных в поставщике контента и метод onChange() никогда не вызывается. В этом случае вам нужно будет самостоятельно реализовать механизм обнаружения изменений в данных устройства. Этот механизм также отвечает за вызов requestSync() при изменении данных.

Чтобы создать наблюдатель для поставщика контента, расширьте класс ContentObserver и реализуйте обе формы его метода onChange(). В onChange() вызовите requestSync(), чтобы запустить адаптер синхронизации.

Чтобы зарегистрировать наблюдателя, передайте его в качестве аргумента в вызове registerContentObserver(). В этом вызове также необходимо передать URI контента для данных, которые вы хотите отслеживать. Фреймворк поставщика контента сравнивает этот URI отслеживания с URI контента, переданными в качестве аргументов в методы ContentResolver, которые изменяют поставщика, например ContentResolver.insert(). Если совпадение найдено, вызывается ваша реализация ContentObserver.onChange().

В следующем фрагменте кода показано, как определить ContentObserver, который вызывает requestSync() при изменении таблицы:

Kotlin

// Constants
// Content provider scheme
const val SCHEME = "content://"
// Content provider authority
const val AUTHORITY = "com.example.android.datasync.provider"
// Path for the content provider table
const val TABLE_PATH = "data_table"
...
class MainActivity : FragmentActivity() {
    ...
    // A content URI for the content provider's data table
    private lateinit var uri: Uri
    // A content resolver for accessing the provider
    private lateinit var mResolver: ContentResolver
    ...
    inner class TableObserver(...) : ContentObserver(...) {
        /*
         * Define a method that's called when data in the
         * observed content provider changes.
         * This method signature is provided for compatibility with
         * older platforms.
         */
        override fun onChange(selfChange: Boolean) {
            /*
             * Invoke the method signature available as of
             * Android platform version 4.1, with a null URI.
             */
            onChange(selfChange, null)
        }

        /*
         * Define a method that's called when data in the
         * observed content provider changes.
         */
        override fun onChange(selfChange: Boolean, changeUri: Uri?) {
            /*
             * Ask the framework to run your sync adapter.
             * To maintain backward compatibility, assume that
             * changeUri is null.
             */
            ContentResolver.requestSync(account, AUTHORITY, null)
        }
        ...
    }
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Get the content resolver object for your app
        mResolver = contentResolver
        // Construct a URI that points to the content provider data table
        uri = Uri.Builder()
                .scheme(SCHEME)
                .authority(AUTHORITY)
                .path(TABLE_PATH)
                .build()
        /*
         * Create a content observer object.
         * Its code does not mutate the provider, so set
         * selfChange to "false"
         */
        val observer = TableObserver(false)
        /*
         * Register the observer for the data table. The table's path
         * and any of its subpaths trigger the observer.
         */
        mResolver.registerContentObserver(uri, true, observer)
        ...
    }
    ...
}

Java

public class MainActivity extends FragmentActivity {
    ...
    // Constants
    // Content provider scheme
    public static final String SCHEME = "content://";
    // Content provider authority
    public static final String AUTHORITY = "com.example.android.datasync.provider";
    // Path for the content provider table
    public static final String TABLE_PATH = "data_table";
    // Account
    public static final String ACCOUNT = "default_account";
    // Global variables
    // A content URI for the content provider's data table
    Uri uri;
    // A content resolver for accessing the provider
    ContentResolver mResolver;
    ...
    public class TableObserver extends ContentObserver {
        /*
         * Define a method that's called when data in the
         * observed content provider changes.
         * This method signature is provided for compatibility with
         * older platforms.
         */
        @Override
        public void onChange(boolean selfChange) {
            /*
             * Invoke the method signature available as of
             * Android platform version 4.1, with a null URI.
             */
            onChange(selfChange, null);
        }
        /*
         * Define a method that's called when data in the
         * observed content provider changes.
         */
        @Override
        public void onChange(boolean selfChange, Uri changeUri) {
            /*
             * Ask the framework to run your sync adapter.
             * To maintain backward compatibility, assume that
             * changeUri is null.
             */
            ContentResolver.requestSync(mAccount, AUTHORITY, null);
        }
        ...
    }
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Get the content resolver object for your app
        mResolver = getContentResolver();
        // Construct a URI that points to the content provider data table
        uri = new Uri.Builder()
                  .scheme(SCHEME)
                  .authority(AUTHORITY)
                  .path(TABLE_PATH)
                  .build();
        /*
         * Create a content observer object.
         * Its code does not mutate the provider, so set
         * selfChange to "false"
         */
        TableObserver observer = new TableObserver(false);
        /*
         * Register the observer for the data table. The table's path
         * and any of its subpaths trigger the observer.
         */
        mResolver.registerContentObserver(uri, true, observer);
        ...
    }
    ...
}

Периодический запуск адаптера синхронизации

Вы можете запускать адаптер синхронизации периодически, задав период времени ожидания между запусками, или запуская его в определенное время дня, или и то, и другое. Периодический запуск адаптера синхронизации позволяет примерно соответствовать интервалу обновления вашего сервера.

Аналогичным образом можно загружать данные с устройства, когда сервер относительно свободен, запланировав запуск адаптера синхронизации на ночь. Большинство пользователей оставляют свои устройства включенными и подключенными к сети на ночь, поэтому это время обычно доступно. Кроме того, устройство не выполняет другие задачи одновременно с адаптером синхронизации. Если вы выберете этот подход, убедитесь, что каждое устройство запускает передачу данных в разное время. Если все устройства запустят адаптер синхронизации одновременно, вы, скорее всего, перегрузите сервер и сети передачи данных оператора сотовой связи.

В целом периодические запуски имеют смысл, если пользователям не нужны мгновенные обновления, но они ожидают регулярных обновлений. Периодические запуски также имеют смысл, если вы хотите сбалансировать доступность актуальных данных с эффективностью меньших запусков адаптера синхронизации, которые не используют ресурсы устройства чрезмерно.

Чтобы адаптер синхронизации запускался через регулярные интервалы, вызовите addPeriodicSync(). Это позволяет запланировать запуск адаптера синхронизации по истечении определенного времени. Поскольку фреймворк адаптера синхронизации должен учитывать выполнение других адаптеров синхронизации и пытается максимально повысить эффективность батареи, прошедшее время может варьироваться на несколько секунд. Кроме того, фреймворк не запустит адаптер синхронизации, если сеть недоступна.

Обратите внимание, что addPeriodicSync() не запускает адаптер синхронизации в определенное время суток. Чтобы адаптер синхронизации запускался примерно в одно и то же время каждый день, используйте повторяющийся будильник в качестве триггера. Подробную информацию о повторяющихся будильниках можно найти в справочной документации по AlarmManager. Если вы используете метод setInexactRepeating(), чтобы задать триггеры времени суток с некоторыми вариациями, вам все равно следует рандомизировать время начала, чтобы адаптеры синхронизации запускались с разных устройств в разное время.

Метод addPeriodicSync() не отключает setSyncAutomatically(), поэтому за короткий период времени может быть выполнено несколько синхронизаций. Кроме того, при вызове addPeriodicSync() можно использовать только некоторые флаги управления адаптером синхронизации. Флаги, которые использовать нельзя, описаны в документации по addPeriodicSync().

В следующем фрагменте кода показано, как запланировать периодические запуски адаптера синхронизации:

Kotlin

// Content provider authority
const val AUTHORITY = "com.example.android.datasync.provider"
// Account
const val ACCOUNT = "default_account"
// Sync interval constants
const val SECONDS_PER_MINUTE = 60L
const val SYNC_INTERVAL_IN_MINUTES = 60L
const val SYNC_INTERVAL = SYNC_INTERVAL_IN_MINUTES * SECONDS_PER_MINUTE
...
class MainActivity : FragmentActivity() {
    ...
    // A content resolver for accessing the provider
    private lateinit var mResolver: ContentResolver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Get the content resolver for your app
        mResolver = contentResolver
        /*
         * Turn on periodic syncing
         */
        ContentResolver.addPeriodicSync(
                mAccount,
                AUTHORITY,
                Bundle.EMPTY,
                SYNC_INTERVAL)
        ...
    }
    ...
}

Java

public class MainActivity extends FragmentActivity {
    ...
    // Constants
    // Content provider authority
    public static final String AUTHORITY = "com.example.android.datasync.provider";
    // Account
    public static final String ACCOUNT = "default_account";
    // Sync interval constants
    public static final long SECONDS_PER_MINUTE = 60L;
    public static final long SYNC_INTERVAL_IN_MINUTES = 60L;
    public static final long SYNC_INTERVAL =
            SYNC_INTERVAL_IN_MINUTES *
            SECONDS_PER_MINUTE;
    // Global variables
    // A content resolver for accessing the provider
    ContentResolver mResolver;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Get the content resolver for your app
        mResolver = getContentResolver();
        /*
         * Turn on periodic syncing
         */
        ContentResolver.addPeriodicSync(
                mAccount,
                AUTHORITY,
                Bundle.EMPTY,
                SYNC_INTERVAL);
        ...
    }
    ...
}

Как запустить адаптер синхронизации по запросу

Запуск адаптера синхронизации в ответ на запрос пользователя – наименее предпочтительная стратегия. Фреймворк специально разработан для экономии заряда батареи при запуске адаптеров синхронизации по расписанию. Варианты, которые запускают синхронизацию в ответ на изменения данных, эффективно используют заряд батареи, поскольку энергия используется для предоставления новых данных.

Если же пользователи будут запускать синхронизацию по запросу, она будет выполняться отдельно, что приведет к неэффективному использованию сетевых ресурсов и электроэнергии. Кроме того, если синхронизация выполняется по запросу, пользователи могут запрашивать ее, даже если данные не изменились. Это приводит к неэффективному расходу заряда батареи. В целом приложение должно либо использовать другие сигналы для запуска синхронизации, либо планировать ее через регулярные интервалы без участия пользователя.

Однако если вы все же хотите запускать адаптер синхронизации по запросу, установите флаги адаптера синхронизации для ручного запуска, а затем вызовите ContentResolver.requestSync().

Выполните перенос по запросу со следующими флагами:

SYNC_EXTRAS_MANUAL
Принудительно запускает синхронизацию вручную. Фреймворк адаптера синхронизации игнорирует существующие настройки, например флаг, заданный методом setSyncAutomatically().
SYNC_EXTRAS_EXPEDITED
Принудительно запускает синхронизацию. Если вы не зададите это значение, система может подождать несколько секунд перед выполнением запроса на синхронизацию, поскольку она пытается оптимизировать использование батареи, планируя множество запросов за короткий период времени.

В следующем фрагменте кода показано, как вызвать requestSync() в ответ на нажатие кнопки:

Kotlin

// Constants
// Content provider authority
val AUTHORITY = "com.example.android.datasync.provider"
// Account type
val ACCOUNT_TYPE = "com.example.android.datasync"
// Account
val ACCOUNT = "default_account"
...
class MainActivity : FragmentActivity() {
    ...
    // Instance fields
    private lateinit var mAccount: Account
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        /*
         * Create the placeholder account. The code for CreateSyncAccount
         * is listed in the lesson Creating a Sync Adapter
         */

        mAccount = createSyncAccount()
        ...
    }

    /**
     * Respond to a button click by calling requestSync(). This is an
     * asynchronous operation.
     *
     * This method is attached to the refresh button in the layout
     * XML file
     *
     * @param v The View associated with the method call,
     * in this case a Button
     */
    fun onRefreshButtonClick(v: View) {
        // Pass the settings flags by inserting them in a bundle
        val settingsBundle = Bundle().apply {
            putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true)
            putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true)
        }
        /*
         * Request the sync for the default account, authority, and
         * manual sync settings
         */
        ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle)
    }

Java

public class MainActivity extends FragmentActivity {
    ...
    // Constants
    // Content provider authority
    public static final String AUTHORITY =
            "com.example.android.datasync.provider";
    // Account type
    public static final String ACCOUNT_TYPE = "com.example.android.datasync";
    // Account
    public static final String ACCOUNT = "default_account";
    // Instance fields
    Account mAccount;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        /*
         * Create the placeholder account. The code for CreateSyncAccount
         * is listed in the lesson Creating a Sync Adapter
         */

        mAccount = CreateSyncAccount(this);
        ...
    }
    /**
     * Respond to a button click by calling requestSync(). This is an
     * asynchronous operation.
     *
     * This method is attached to the refresh button in the layout
     * XML file
     *
     * @param v The View associated with the method call,
     * in this case a Button
     */
    public void onRefreshButtonClick(View v) {
        // Pass the settings flags by inserting them in a bundle
        Bundle settingsBundle = new Bundle();
        settingsBundle.putBoolean(
                ContentResolver.SYNC_EXTRAS_MANUAL, true);
        settingsBundle.putBoolean(
                ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
        /*
         * Request the sync for the default account, authority, and
         * manual sync settings
         */
        ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle);
    }