package com.boardgamegeek.auth;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.NetworkErrorException;
import android.accounts.OperationCanceledException;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.widget.Toast;
import com.boardgamegeek.R;
import com.boardgamegeek.ui.LoginActivity;
import com.boardgamegeek.util.ActivityUtils;
import java.io.IOException;
import timber.log.Timber;
public class Authenticator extends AbstractAccountAuthenticator {
public static final String ACCOUNT_TYPE = "com.boardgamegeek";
public static final String AUTH_TOKEN_TYPE = "com.boardgamegeek";
public static final String KEY_AUTH_TOKEN_EXPIRY = "AUTHTOKEN_EXPIRY";
public static final String KEY_USER_ID = "com.boardgamegeek.USER_ID";
public static final String INVALID_USER_ID = "0";
private final Context context;
public Authenticator(Context context) {
super(context);
this.context = context;
}
@NonNull
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Timber.v("Adding account: accountType=%s, authTokenType=%s", accountType, authTokenType);
return createLoginIntent(response, null);
}
@Nullable
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
Timber.v("confirmCredentials is not supported. If it was we would ask the user for their password.");
return null;
}
@NonNull
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
Timber.v("editProperties is not supported.");
throw new UnsupportedOperationException();
}
@NonNull
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, @NonNull Account account, @NonNull String authTokenType, Bundle options) throws NetworkErrorException {
Timber.v("getting auth token...");
// If the caller requested an authToken type we don't support, then return an error
if (!authTokenType.equals(Authenticator.AUTH_TOKEN_TYPE)) {
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType");
return result;
}
final AccountManager am = AccountManager.get(context);
// Return the cached auth token (unless expired)
String authToken = am.peekAuthToken(account, authTokenType);
if (!TextUtils.isEmpty(authToken)) {
if (!isKeyExpired(am, account, KEY_AUTH_TOKEN_EXPIRY)) {
Timber.v(toDebugString());
return createAuthTokenBundle(account, authToken);
}
am.invalidateAuthToken(authTokenType, authToken);
}
// Ensure the password is valid and not expired, then return the stored AuthToken
final String password = am.getPassword(account);
if (!TextUtils.isEmpty(password)) {
BggCookieJar cookieJar = NetworkAuthenticator.authenticate(account.name, password, "Renewal");
if (cookieJar != null) {
am.setAuthToken(account, authTokenType, cookieJar.getAuthToken());
am.setUserData(account, Authenticator.KEY_AUTH_TOKEN_EXPIRY, String.valueOf(cookieJar.getAuthTokenExpiry()));
Timber.v(toDebugString());
return createAuthTokenBundle(account, cookieJar.getAuthToken());
}
}
// If we get here, then we couldn't access the user's password - so we need to re-prompt them for their
// credentials. We do that by creating an intent to display our AuthenticatorActivity panel.
Timber.i("Expired credentials...");
return createLoginIntent(response, account.name);
}
@Nullable
@Override
public String getAuthTokenLabel(String authTokenType) {
Timber.v("getAuthTokenLabel - we don't support multiple auth tokens");
return null;
}
@NonNull
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
Timber.v("hasFeatures - we don't support any features");
final Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
return result;
}
@Nullable
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
Timber.v("updateCredentials is not supported. If it was we would ask the user for their password.");
return null;
}
/**
* Gets the account associated with BoardGameGeek. Returns null if their is a problem getting the account.
*/
@Nullable
public static Account getAccount(Context context) {
if (context != null) {
return getAccount(AccountManager.get(context));
}
return null;
}
/**
* Gets the account associated with BoardGameGeek. Returns null if their is a problem getting the account.
*/
@Nullable
public static Account getAccount(@NonNull AccountManager accountManager) {
Account[] accounts = accountManager.getAccountsByType(Authenticator.ACCOUNT_TYPE);
if (accounts.length == 0) {
// likely the user has never signed in
Timber.v("no account!");
return null;
} else if (accounts.length != 1) {
Timber.w("multiple accounts!");
return null;
}
return accounts[0];
}
/**
* Get the BGG user ID of the authenticated user.
*/
public static String getUserId(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = getAccount(accountManager);
if (account == null) {
return INVALID_USER_ID;
}
String userId = accountManager.getUserData(account, KEY_USER_ID);
if (userId == null) {
return INVALID_USER_ID;
}
return userId;
}
/**
* Determines if the user is signed in.
*/
public static boolean isSignedIn(Context context) {
return getAccount(context) != null;
}
public static void clearPassword(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = getAccount(accountManager);
if (account != null) {
String authToken = accountManager.peekAuthToken(account, Authenticator.AUTH_TOKEN_TYPE);
if (authToken != null) {
accountManager.invalidateAuthToken(Authenticator.AUTH_TOKEN_TYPE, authToken);
} else {
accountManager.clearPassword(account);
}
}
}
public static boolean isOldAuth(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = getAccount(accountManager);
String data = accountManager.getUserData(account, "PASSWORD_EXPIRY");
return data != null;
}
public static long getLong(Context context, String key) {
return getLong(context, key, 0);
}
public static long getLong(Context context, String key, long defaultValue) {
if (context == null) {
return defaultValue;
}
AccountManager accountManager = AccountManager.get(context);
Account account = getAccount(accountManager);
if (account == null) {
return defaultValue;
}
return getLong(accountManager, account, key, defaultValue);
}
public static long getLong(@NonNull AccountManager accountManager, Account account, String key) {
String s = accountManager.getUserData(account, key);
return TextUtils.isEmpty(s) ? 0 : Long.parseLong(s);
}
public static long getLong(@NonNull AccountManager accountManager, Account account, String key, long defaultValue) {
String s = accountManager.getUserData(account, key);
return TextUtils.isEmpty(s) ? defaultValue : Long.parseLong(s);
}
public static void putLong(Context context, String key, long value) {
AccountManager accountManager = AccountManager.get(context);
Account account = getAccount(accountManager);
if (account != null) {
accountManager.setUserData(account, key, String.valueOf(value));
}
}
public static void putInt(Context context, String key, int value) {
AccountManager accountManager = AccountManager.get(context);
Account account = getAccount(accountManager);
if (account != null) {
accountManager.setUserData(account, key, String.valueOf(value));
}
}
public static void signOut(final Context context) {
AccountManager am = AccountManager.get(context);
final Account account = Authenticator.getAccount(am);
if (account != null) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
removeAccountWithActivity(context, am, account);
} else {
removeAccount(context, am, account);
}
}
AccountUtils.clearFields(context);
}
@NonNull
private Bundle createAuthTokenBundle(@NonNull Account account, String authToken) {
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, Authenticator.ACCOUNT_TYPE);
result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
return result;
}
@NonNull
private Bundle createLoginIntent(AccountAuthenticatorResponse response, String accountName) {
final Intent intent = new Intent(context, LoginActivity.class);
if (!TextUtils.isEmpty(accountName)) {
intent.putExtra(ActivityUtils.KEY_USERNAME, accountName);
}
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
private boolean isKeyExpired(@NonNull final AccountManager am, Account account, @SuppressWarnings("SameParameterValue") String key) {
String expiration = am.getUserData(account, key);
return !TextUtils.isEmpty(expiration) && Long.valueOf(expiration) < System.currentTimeMillis();
}
@SuppressWarnings("deprecation")
private static void removeAccount(final Context context, @NonNull AccountManager am, Account account) {
am.removeAccount(account, new AccountManagerCallback<Boolean>() {
@Override
public void run(@NonNull AccountManagerFuture<Boolean> future) {
if (future.isDone()) {
try {
if (future.getResult()) {
Toast.makeText(context, R.string.msg_sign_out_success, Toast.LENGTH_LONG).show();
}
} catch (@NonNull OperationCanceledException | AuthenticatorException | IOException e) {
Timber.e(e, "removeAccount");
}
}
}
}, null);
}
@TargetApi(VERSION_CODES.LOLLIPOP_MR1)
private static void removeAccountWithActivity(final Context context, @NonNull AccountManager am, Account account) {
am.removeAccount(account, null, new AccountManagerCallback<Bundle>() {
@Override
public void run(@NonNull AccountManagerFuture<Bundle> future) {
if (future.isDone()) {
try {
if (future.getResult().getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) {
Toast.makeText(context, R.string.msg_sign_out_success, Toast.LENGTH_LONG).show();
}
} catch (@NonNull OperationCanceledException | AuthenticatorException | IOException e) {
Timber.e(e, "removeAccount");
}
}
}
}, null);
}
@NonNull
private String toDebugString() {
if (context == null) {
return "";
}
AccountManager accountManager = AccountManager.get(context);
if (accountManager == null) {
return "";
}
Account account = getAccount(accountManager);
if (account == null) {
return "";
}
return "ACCOUNT" + "\n" +
"Name: " + account.name + "\n" +
"Type: " + account.type + "\n" +
"Password: " + accountManager.getPassword(account) + "\n";
}
}