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

от автора

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 если быть точным). Но для нас, на данный момент, представляют интерес всего 2.

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. Это то о чем я говорил немного выше. Это еще одна плюшка в довесок к остальным — возможность хранить профиль пользователя без необходимости создания/использования внутренних хранилищ.

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

ссылка на оригинал статьи http://habrahabr.ru/company/e-Legion/blog/206210/