...

пятница, 21 марта 2014 г.

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


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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий