Лидер мобильной разработки в России
118,00
рейтинг
21 марта 2014 в 13:42

Разработка → Синхронизация в Android приложениях. Часть первая tutorial

image
На дворе 2014 год, доля Android JellyBean перевалила за 60%, появились новые тренды в дизайне. В общем, случилось много всего интересного. Но синхронизация данных с сервером осталось неотъемлемой частью большинства приложений. Существует много способов реализации ее в приложении. Android предоставляет нам SyncAdapter Framework, который позволяет автоматизировать и координировать этот процесс и предоставляет множество плюшек в довесок.

Account


Для начала нам потребуется собственный аккаунт на устройстве. Сначала, я думаю, стоит ответить на вопрос, зачем? Действительно, зачем?

Краткое резюме преимуществ:
  • Поддержка фоновых механизмов вроде SyncAdapter
  • Стандартизация способа авторизации
  • Поддержка различных токенов (прав доступа)
  • Шаринг аккаунта с разграничением привилегий (возможность использовать один аккаунт для различных приложений, как это делает Google)

Шаги для получения плюшек:
1) Создание Authenticator'а
2) Создание Activity для логина
3) Создание сервиса для общения с нашим аккаунтом


AccountManager — управляет аккаунтами устройства. Приложения запрашивают авторизационные токены именно у него.

AbstractAccountAuthenticator — компонент для работы с определенным типом аккаунта. Вся механика по работе с аккаунтом (авторизация, разграничение прав) осуществляется здесь. Может быть общим для различных приложений. AccountManager работает именно с ним.

AccountAuthenticatorActivity — базовый класс активити для авторизации/создания аккаунта. Вызывается AccountManager'ом в случае необходимости идентифицировать аккаунт (токен отсутствует или протух).

Как это все работает, можно посмотреть на диаграмме из документации
image

Когда нам понадобился токен, мы работаем с методом AccountManager'а — getAuthToken. Стоит заметить, что это асинхронный метод и его можно безопасно вызывать из UI потока. Существует также синхронная версия этого метода — blockingGetAuthToken. К диаграмме еще вернемся.

Создание Authenticator'а


Для создания собственного Authenticator'а, нам необходимо расширить AbstractAccountAuthenticator и реализовать несколько его методов (7 если быть точным). Но для нас, на данный момент, представляют интерес всего два.

addAccount
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
                         String[] requiredFeatures, Bundle options)
        throws NetworkErrorException {
    final Intent intent = new Intent(mContext, NewAccountActivity.class);
    intent.putExtra(NewAccountActivity.EXTRA_TOKEN_TYPE, accountType);
    intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
    final Bundle bundle = new Bundle();
    if (options != null) {
        bundle.putAll(options);
    }
    bundle.putParcelable(AccountManager.KEY_INTENT, intent);
    return bundle;
}


Метод, как видно из названия, вызывается при попытке добавить новый аккаунт. Все, что мы должны в нем сделать — это вернуть Intent, который должен запустить наше Activity. Чтобы иметь возможность добавить аккаунт из приложения, нам потребуются соответствующие разрешения.
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />


getAuthToken
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType,
                           Bundle options) throws NetworkErrorException {
    final Bundle result = new Bundle();
    final AccountManager am = AccountManager.get(mContext.getApplicationContext());
    String authToken = am.peekAuthToken(account, authTokenType);
    if (TextUtils.isEmpty(authToken)) {
        final String password = am.getPassword(account);
        if (!TextUtils.isEmpty(password)) {
            authToken = AuthTokenLoader.signIn(mContext, account.name, password);
        }
    }
    if (!TextUtils.isEmpty(authToken)) {
        result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
        result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
        result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
    } else {
        final Intent intent = new Intent(mContext, LoginActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        intent.putExtra(LoginActivity.EXTRA_TOKEN_TYPE, authTokenType);
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
    }
    return result;
}


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

Создание Activity авторизации


Наше активити должно наследоваться от AccountAuthenticatorActivity (строго говоря, не должно а может: в AccountAuthenticatorActivity 20 строчек вспомогательного кода, который можно написать руками в любом другом активити). У нас будет самое простое активити с полями логин/пароль и кнопкой войти. В целом, в AccountManager'е можно сохранять произвольную информацию о профиле пользователя. Отвечать за получение токена будет AuthTokenLoader, но можно использовать любой понравившийся механизм. Задача-то простая — получить от сервера токен.

onTokenReceived
public void onTokenReceived(Account account, String password, String token) {
    final AccountManager am = AccountManager.get(this);
    final Bundle result = new Bundle();
    if (am.addAccountExplicitly(account, password, new Bundle())) {
        result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
        result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
        result.putString(AccountManager.KEY_AUTHTOKEN, token);
        am.setAuthToken(account, account.type, token);
    } else {
        result.putString(AccountManager.KEY_ERROR_MESSAGE, getString(R.string.account_already_exists));
    }
    setAccountAuthenticatorResult(result);
    setResult(RESULT_OK);
    finish();
}


Данный метод вызывается, когда токен от сервера получен (а это говорит о валидности аккаунта) и, соответственно, можно добавить аккаунт на устройство. setAccountAuthenticatorResult — метод для передачи результата обратно в AccountManager.

Сервис для интергации в систему


Сервис позволит системе и другим приложениям связываться с нашим Authenticator'ом. Код сервиса максимально прост:
GitHubAuthenticatorService
public class GitHubAuthenticatorService extends Service {

    private GitHubAuthenticator mAuthenticator;

    @Override
    public void onCreate() {
        super.onCreate();
        mAuthenticator = new GitHubAuthenticator(getApplicationContext());
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mAuthenticator.getIBinder();
    }

}


Все, что он делает, это возвращает IBinder нашего Authenticator'a. Причем метод getIBinder уже реализован в AbstractAccountAuthenticator. Осталось только прописать наш сервис в манифесте приложения.
<service
    android:name=".account.GitHubAuthenticatorService"
    android:exported="false">
    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator" />
    </intent-filter>
    <meta-data
        android:name="android.accounts.AccountAuthenticator"
        android:resource="@xml/github_authenticator" />
</service>

Осталась совсем маленькая деталь: вы могли заметить такую строчку
android:resource="@xml/github_authenticator"

Это метафайл, который описывает наш Authenticator. Его необходимо создать в папке res/xml. В нем мы указываем иконку нашего аккаунта, его название и тип. В самом простом случае, он выглядит так:
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.github.elegion"
    android:icon="@drawable/ic_github"
    android:label="@string/github"
    android:smallIcon="@drawable/ic_github" />

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

P.S. В качестве бонуса: у AccountManager'а есть метод setUserData(final Account account, final String key, final String value) который по сути предоставляет нам возможность хранения любой информации в формате key-value. Это то, о чем я говорил немного выше. Это еще одна плюшка в довесок к остальным — возможность хранить профиль пользователя без необходимости создания/использования внутренних хранилищ.

Исходники проекта можно взять тут.

Синхронизация в Android приложениях. Часть вторая
Автор: @dev_troy
e-Legion Ltd.
рейтинг 118,00
Лидер мобильной разработки в России

Комментарии (11)

  • 0
    Спасибо, очень полезно.
  • +1
    Спасибо! Буквально пару дней назад задавал вопрос по поводу аутентификации на stackoverflow. В ответ дали ссылку на хороший туториал.
  • +2
    Спасибо за статью! Когда-то с этим разбирался и нашёл 2 очень хорошие статьи на английском:
    1. Об Authenticator'е http://udinic.wordpress.com/2013/04/24/write-your-own-android-authenticator/
    2. О SyncAdaptor'е http://udinic.wordpress.com/2013/07/24/write-your-own-android-sync-adapter/
  • 0
    Надеюсь, в следующей части будет покрыт и случай, когда синхронизация нужна, а авторизация — нет (клиент просто регулярно скачивает обновленную информацию с сервера).
    • 0
      Тут есть вся информация об этом, ну и пример достаточно показателен.
      • 0
        Да, информации полно, если поискать. Но суть статей на харбе в централизации этой самой информации и доступе к ней на великом и могучем.
        • 0
          Информации много, да. Но всё-таки было бы интересно прочесть «часть вторую» именно в вашей интерпретации :)
          • 0
            Если интересно в моей — вот. С пылу, с жару =)
  • 0
    Можете дать небольшой комментарий по классу GitHubAuthenticator?
    В else-блоке метода getAuthToken(...) создается Intent, а следом создается Bundle, в который помещается созданный Intent. Но метод возвращает другой Bundle (который result, созданный в начале метода). То есть, созданные в else-блоке Intent и Bundle нив чем не участвуют. Может, подразумевалось помещение Intent в тот Bundle, который result, а не тот, который bundle?
  • 0
    «у AccountManager'а есть метод setUserData(final Account account, final String key, final String value) который по сути предоставляет нам возможность хранения любой информации в формате key-value. Это то, о чем я говорил немного выше. Это еще одна плюшка в довесок к остальным — возможность хранить профиль пользователя без необходимости создания/использования внутренних хранилищ»

    Как клиентскому приложению затем получить доступ к профилю? Эти методы доступны из приложения с аутентификатором — да. Но пусть клиентское приложение имеет другую подпись, вызовы getUserData() кинут exception (из документации — «This method requires the caller <...> to have the same UID as the account's authenticator»). Как тогда клиентскому приложению получить эти данные?
    • 0
      Избежать ошибки «Activity to be started with KEY_INTENT must share Authenticator's signatures» можно если в манифесте аутентикактора в XML указать android:customTokens=«true». Интересен другой вопрос — как работать с AccountManager из бекграунд-сервиса (в частности как вызвать метод getAuthTokenByFeatures так чтоб он показал логин активити при необходимости)? Похоже без хаков с dummy активини не вийдет.
      (наверное ответ достоин награды Necromancer :D )

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка