package com.dozuki.ifixit.model.auth; import android.accounts.AbstractAccountAuthenticator; import android.accounts.Account; import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; import android.accounts.NetworkErrorException; import android.content.Context; import android.os.Bundle; import android.util.Log; import com.dozuki.ifixit.BuildConfig; import com.dozuki.ifixit.model.dozuki.Site; import com.dozuki.ifixit.model.user.User; import com.dozuki.ifixit.util.api.ApiContentProvider; /** * This is the authenticator for accounts associated with this particular app. * Accounts are not shared between white-labelled apps e.g. iFixit vs. Dozuki. * Many of the "important" methods aren't implemented because they are only used * when sharing accounts between apps and retrieving auth tokens in a consistent * manner. Since we already had all of this functionality, the main benefit of * using the AccountManager system is for background syncing. The SyncService * API requires an Account so it makes sense to store all of the relevant user * data (email, auth token, userid, etc.) in this system rather than in * SharedPreferences. * * This class also has lots of helper methods for interacting with * AccountManager so our application doesn't need to use it directly. */ public class Authenticator extends AbstractAccountAuthenticator { public static final String AUTH_TOKEN_TYPE_FULL_ACCESS = "Full access"; private static final String USER_DATA_SITE_NAME = "USER_DATA_SITE_NAME"; private static final String USER_DATA_USER_NAME = "USER_DATA_USER_NAME"; private static final String USER_DATA_USERID = "USER_DATA_USERID"; private static final String USER_DATA_EMAIL = "USER_DATA_EMAIL"; private final Context mContext; private final AccountManager mAccountManager; public Authenticator(Context context) { super(context); mContext = context; mAccountManager = AccountManager.get(mContext); } public static String getAccountType() { return "com.dozuki." + BuildConfig.SITE_NAME; } /** * Call whenever an account has been authenticated so it can be added to the * AccountManager with all of the expected fields. Removes any accounts that * are associated with the same site and updates the account if we suspect * it's the same user. */ public Account onAccountAuthenticated(Site site, String email, String userName, int userid, String password, String authToken) { if (!site.reauthenticateOnLogout()) { // Don't store the password if the user shouldn't be reauthenticated. password = ""; } Bundle userData = getUserDataBundle(site, email, userName, userid); Account existingAccount = getAccountForSite(site); if (existingAccount != null) { if (email.equals(mAccountManager.getUserData(existingAccount, USER_DATA_EMAIL))) { return updateAccount(existingAccount, password, authToken, userData); } else { // Remove the existing account because we will make a new one below. We only // allow at most 1 account per site. removeAccount(existingAccount); } } // Accounts cannot share the same name so we must prefix the username with the site // name if this is the dozuki app. String accountName = userName; if (BuildConfig.SITE_NAME.equals("dozuki")) { accountName = site.mTitle + ": " + userName; } Account newAccount = new Account(accountName, getAccountType()); mAccountManager.addAccountExplicitly(newAccount, password, userData); mAccountManager.setAuthToken(newAccount, AUTH_TOKEN_TYPE_FULL_ACCESS, authToken); // By default, automatically sync user's data. mContext.getContentResolver().setSyncAutomatically(newAccount, ApiContentProvider.getAuthority(), true); return newAccount; } private Account updateAccount(Account account, String password, String authToken, Bundle userData) { // Unfortunately you can't set a bundle on an existing account so we // must iterate over the keys and set the data one by one. for (String key : userData.keySet()) { mAccountManager.setUserData(account, key, userData.getString(key)); } mAccountManager.setPassword(account, password); mAccountManager.setAuthToken(account, AUTH_TOKEN_TYPE_FULL_ACCESS, authToken); return account; } private Bundle getUserDataBundle(Site site, String email, String userName, int userid) { Bundle userData = new Bundle(); userData.putString(USER_DATA_SITE_NAME, site.mName); userData.putString(USER_DATA_EMAIL, email); userData.putString(USER_DATA_USER_NAME, userName); userData.putString(USER_DATA_USERID, "" + userid); return userData; } public void invalidateAuthToken(String authToken) { mAccountManager.invalidateAuthToken(AUTH_TOKEN_TYPE_FULL_ACCESS, authToken); } public Account getAccountForSite(Site site) { String siteName = site.mName; for (Account account : mAccountManager.getAccountsByType(getAccountType())) { if (mAccountManager.getUserData(account, USER_DATA_SITE_NAME).equals(siteName)) { return account; } } return null; } public String getPassword(Account account) { return mAccountManager.getPassword(account); } /** * Factory method for creating a User from an Account. */ public User createUser(Account account) { User user = new User(); /** * The auth token will be invalidated if the user's auth token expired and * the stored credentials could not successfully reauthenticate. In this case * we pretend that the user is still signed in with a valid account. The * next request that requires authentication will trigger the login dialog. */ String authToken = mAccountManager.peekAuthToken(account, AUTH_TOKEN_TYPE_FULL_ACCESS); user.setAuthToken(authToken == null ? "invalid" : authToken); user.setUsername(mAccountManager.getUserData(account, USER_DATA_USER_NAME)); user.setUserid(Integer.parseInt(mAccountManager.getUserData(account, USER_DATA_USERID))); user.mEmail = mAccountManager.getUserData(account, USER_DATA_EMAIL); user.mSiteName = mAccountManager.getUserData(account, USER_DATA_SITE_NAME); return user; } public void removeAccount(Account account) { mAccountManager.removeAccount(account, null, null); } /** * Unimplemented methods. Turns out we don't really need to implement any of * these methods unless we plan on sharing accounts outside of our app. */ @Override public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { Log.w("Authenticator", "addAccount not implemented"); // Creates an Intent to start the authentication activity. return null; } @Override public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { Log.w("Authenticator", "getAuthToken not implemented"); return null; } @Override public String getAuthTokenLabel(String authTokenType) { return authTokenType; } @Override public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { Log.w("Authenticator", "hasFeatures not implemented"); return null; } @Override public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { Log.w("Authenticator", "confirmCredentials not implemented"); return null; } @Override public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { Log.w("Authenticator", "updateCredentials not implemented"); return null; } @Override public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { Log.w("Authenticator", "editProperties not implemented"); return null; } }