На дворе 2014 год, доля Android JellyBean перевалила за 60%, появились новые тренды в дизайне. В общем, случилось много всего интересного. Но синхронизация данных с сервером осталось неотъемлемой частью большинства приложения. Существует много способов реализации ее в приложении. Android предоставляет нам SyncAdapter Framework, который позволяет автоматизировать и координировать этот процесс и предоставляет множество плюшек в довесок.
Account
Для начала нам потребуется собственный аккаунт на устройстве. Сначала, я думаю, стоит ответить на вопрос, зачем? Действительно, зачем?
Краткое резюме преимуществ:
- Поддержка фоновых механизмов вроде SyncAdapter
- Стандартизация способа авторизации
- Поддержка различных токенов (прав доступа)
- Шаринг аккаунта с разграничением привилегий (возможность использовать один аккаунт для различных приложения, как это делает Google)
Шаги для получения плюшек:
1) Создание Authenticator'а
2) Создание Activity для логина
3) Создание сервиса для общения с нашим аккаунтом
AccountManager — управляет аккаунтами устройства. Приложения запрашивают авторизационные токены именно у него.
AbstractAccountAuthenticator — компонент для работы с определенным типом аккаунта. Вся механика по работе с аккаунтом (авторизация, разграничение прав) осуществляется здесь. Может быть общим для различных приложений. AccountManager работает именно с ним.
AccountAuthenticatorActivity — базовый класс активити для авторизации/создания аккаунта. Вызывается AccountManager'ом в случае необходимости идентифицировать аккаунт (токен отсутствует или протух).
Как это все работает, можно посмотреть на диаграмме из документации
Когда нам понадобился токен, мы работаем с методом AccountManager'а — getAuthToken. Стоит заметить, что это асинхронный метод и его можно безопасно вызывать из UI потока. Существует также синхронная версия этого метода — blockingGetAuthToken. К диаграмме еще вернемся.
Создание Authenticator'а
Для создания собственного Authenticator'а, нам необходимо расширить AbstractAccountAuthenticator и реализовать несколько его методов (7 если быть точным). Но для нас, на данный момент, представляют интерес всего два.
@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" />
@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, но можно использовать любой понравившийся механизм. Задача-то простая — получить от сервера токен.
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'ом. Код сервиса максимально прост:
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. Это то, о чем я говорил немного выше. Это еще одна плюшка в довесок к остальным — возможность хранить профиль пользователя без необходимости создания/использования внутренних хранилищ.
Исходники проекта можно взять тут.
Автор: dev_troy