/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> === * * Copyright 2012, Christopher Reichert <creichert07@gmail.com> * Copyright 2013, Enno Gottschalk <mrmaffen@googlemail.com> * * Tomahawk is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Tomahawk is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>. */ package org.tomahawk.libtomahawk.authentication; import org.jdeferred.Promise; import org.tomahawk.libtomahawk.authentication.models.HatchetAuthResponse; import org.tomahawk.libtomahawk.infosystem.InfoRequestData; import org.tomahawk.libtomahawk.infosystem.InfoSystem; import org.tomahawk.libtomahawk.infosystem.User; import org.tomahawk.libtomahawk.utils.ADeferredObject; import org.tomahawk.libtomahawk.utils.GsonHelper; import org.tomahawk.libtomahawk.utils.VariousUtils; import org.tomahawk.tomahawk_android.R; import org.tomahawk.tomahawk_android.TomahawkApp; import org.tomahawk.tomahawk_android.utils.ThreadManager; import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import java.util.HashSet; import java.util.List; import de.greenrobot.event.EventBus; import retrofit.RestAdapter; import retrofit.RetrofitError; import retrofit.converter.GsonConverter; public class HatchetAuthenticatorUtils extends AuthenticatorUtils { private static final String TAG = HatchetAuthenticatorUtils.class.getSimpleName(); public static final String HATCHET_PRETTY_NAME = "Hatchet"; public static final String ACCOUNT_TYPE = "is.hatchet.account"; private static final String AUTH_TOKEN_HATCHET = "is.hatchet.account.authtoken"; private static final String AUTH_TOKEN_EXPIRES_IN_HATCHET = "is.hatchet.account.authtokenexpiresin"; private static final String MANDELLA_ACCESS_TOKEN_HATCHET = "is.hatchet.account.mandellaaccesstoken"; private static final String MANDELLA_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET = "is.hatchet.account.mandellaaccesstokenexpiresin"; private static final String CALUMET_ACCESS_TOKEN_HATCHET = "is.hatchet.account.calumetaccesstoken"; private static final String CALUMET_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET = "is.hatchet.account.calumetaccesstokenexpiresin"; private static final String USER_ID_HATCHET = "is.hatchet.account.userid"; private static final String HATCHET_AUTH_BASE_URL = "https://auth.hatchet.is/v1"; private static final String PARAMS_GRANT_TYPE_PASSWORD = "password"; private static final String PARAMS_GRANT_TYPE_REFRESHTOKEN = "refresh_token"; private static final String RESPONSE_TOKEN_TYPE_BEARER = "bearer"; private static final String RESPONSE_TOKEN_TYPE_CALUMET = "calumet"; private static final String RESPONSE_ERROR_INVALID_REQUEST = "invalid_request"; private static final int EXPIRING_LIMIT = 300; private final HatchetAuth mHatchetAuth; private final HashSet<String> mCorrespondingRequestIds = new HashSet<>(); private ADeferredObject<String, Throwable, Void> mGetUserIdPromise; boolean mWaitingForAccountRemoval; public static class UserLoginEvent { } public HatchetAuthenticatorUtils() { super(TomahawkApp.PLUGINNAME_HATCHET, HATCHET_PRETTY_NAME); EventBus.getDefault().register(this); RestAdapter restAdapter = new RestAdapter.Builder() .setLogLevel(RestAdapter.LogLevel.BASIC) .setEndpoint(HATCHET_AUTH_BASE_URL) .setConverter(new GsonConverter(GsonHelper.get())) .build(); mHatchetAuth = restAdapter.create(HatchetAuth.class); } @SuppressWarnings("unused") public void onEventAsync(InfoSystem.ResultsEvent event) { if (event.mSuccess && mCorrespondingRequestIds.contains(event.mInfoRequestData.getRequestId())) { if (event.mInfoRequestData.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_USERS) { List<User> users = event.mInfoRequestData.getResultList(User.class); if (users != null && users.get(0) != null) { String userId = users.get(0).getId(); storeUserId(userId); mGetUserIdPromise.resolve(userId); } } } } public void onLogin(String username, String refreshToken, long refreshTokenExpiresIn, String accessToken, long accessTokenExpiresIn) { Log.d(TAG, "Hatchet user '" + username + "' logged in successfully :)"); if (username != null && !TextUtils.isEmpty(username) && refreshToken != null && !TextUtils.isEmpty(refreshToken)) { Log.d(TAG, "Hatchet auth token is served and yummy"); Account account = new Account(username, ACCOUNT_TYPE); AccountManager am = AccountManager.get(TomahawkApp.getContext()); if (am != null) { am.addAccountExplicitly(account, null, new Bundle()); am.setAuthToken(account, AUTH_TOKEN_HATCHET, refreshToken); am.setUserData(account, AUTH_TOKEN_EXPIRES_IN_HATCHET, String.valueOf(refreshTokenExpiresIn)); am.setUserData(account, MANDELLA_ACCESS_TOKEN_HATCHET, accessToken); am.setUserData(account, MANDELLA_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET, String.valueOf(accessTokenExpiresIn)); ensureAccessTokens(); } } AuthenticatorManager.ConfigTestResultEvent event = new AuthenticatorManager.ConfigTestResultEvent(); event.mComponent = this; event.mType = AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_SUCCESS; EventBus.getDefault().post(event); AuthenticatorManager.showToast(getPrettyName(), event); } public void onLoginFailed(int type, String message) { Log.d(TAG, "Hatchet login failed :(, Type:" + type + ", Error: " + message); AuthenticatorManager.ConfigTestResultEvent event = new AuthenticatorManager.ConfigTestResultEvent(); event.mComponent = this; event.mType = type; event.mMessage = message; EventBus.getDefault().post(event); AuthenticatorManager.showToast(getPrettyName(), event); } public void onLogout() { Log.d(TAG, "Hatchet user logged out"); AuthenticatorManager.ConfigTestResultEvent event = new AuthenticatorManager.ConfigTestResultEvent(); event.mComponent = this; event.mType = AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_LOGOUT; EventBus.getDefault().post(event); AuthenticatorManager.showToast(getPrettyName(), event); } @Override public String getDescription() { return TomahawkApp.getContext().getString(R.string.preferences_hatchet_text, HATCHET_PRETTY_NAME); } @Override public int getIconResourceId() { return R.drawable.ic_hatchet; } @Override public int getUserIdEditTextHintResId() { return R.string.login_username; } @Override public void register(final String name, final String password, final String email) { ThreadManager.get().execute( new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_AUTHENTICATING) { @Override public void run() { try { HatchetAuthResponse authResponse = mHatchetAuth.registerDirectly(name, password, email); if (authResponse != null) { onLogin(name, authResponse.refresh_token, authResponse.refresh_token_expires_in, authResponse.access_token, authResponse.expires_in); } else { onLoginFailed( AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_COMMERROR, ""); } } catch (RetrofitError e) { Log.d(TAG, "register: " + e.getClass() + ": " + e.getLocalizedMessage()); try { HatchetAuthResponse authResponse = (HatchetAuthResponse) e.getBodyAs(HatchetAuthResponse.class); if (authResponse != null && authResponse.error != null && authResponse.error.equals(RESPONSE_ERROR_INVALID_REQUEST)) { onLoginFailed( AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, authResponse.error_description); } else { onLoginFailed( AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_COMMERROR, ""); } } catch (RuntimeException e1) { onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, "Hatchet authentication error. Sorry, please try again later."); } } } } ); } @Override public void login(final String name, final String password) { ThreadManager.get().execute( new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_AUTHENTICATING) { @Override public void run() { try { HatchetAuthResponse authResponse = mHatchetAuth .login(name, password, PARAMS_GRANT_TYPE_PASSWORD); if (authResponse != null) { onLogin(authResponse.canonical_username, authResponse.refresh_token, authResponse.refresh_token_expires_in, authResponse.access_token, authResponse.expires_in); } else { onLoginFailed( AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_COMMERROR, ""); } } catch (RetrofitError e) { Log.d(TAG, "login: " + e.getClass() + ": " + e.getLocalizedMessage()); try { HatchetAuthResponse authResponse = (HatchetAuthResponse) e.getBodyAs(HatchetAuthResponse.class); if (authResponse != null && authResponse.error != null && authResponse.error.equals(RESPONSE_ERROR_INVALID_REQUEST)) { onLoginFailed( AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_INVALIDCREDS, authResponse.error_description); } else { onLoginFailed( AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_COMMERROR, ""); } } catch (RuntimeException e1) { onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, "Hatchet authentication error. Sorry, please try again later."); } } } } ); } @Override public void logout() { final AccountManager am = AccountManager.get(TomahawkApp.getContext()); if (am != null && getAccount() != null) { am.removeAccount(getAccount(), null, null); mWaitingForAccountRemoval = true; am.addOnAccountsUpdatedListener(new OnAccountsUpdateListener() { @Override public void onAccountsUpdated(Account[] accounts) { if (mWaitingForAccountRemoval && getAccount() == null) { am.removeOnAccountsUpdatedListener(this); mWaitingForAccountRemoval = false; onLogout(); } } }, null, false); } } public boolean isLoggedIn() { AccountManager am = AccountManager.get(TomahawkApp.getContext()); return am != null && getAccount() != null && am.peekAuthToken(getAccount(), AUTH_TOKEN_HATCHET) != null; } public String getUserName() { if (getAccount() != null) { return getAccount().name; } return null; } @Override public boolean doesAllowRegistration() { return true; } public Promise<String, Throwable, Void> getUserId() { ADeferredObject<String, Throwable, Void> getUserIdPromise = mGetUserIdPromise; if (getUserIdPromise == null) { getUserIdPromise = new ADeferredObject<>(); AccountManager am = AccountManager.get(TomahawkApp.getContext()); if (am != null && getAccount() != null) { if (am.getUserData(getAccount(), USER_ID_HATCHET) != null) { getUserIdPromise.resolve(am.getUserData(getAccount(), USER_ID_HATCHET)); } else { String requestId = InfoSystem.get().resolveUserId(getUserName()); if (requestId != null) { mCorrespondingRequestIds.add(requestId); } } } else { getUserIdPromise.reject(new Throwable("No account present.")); mGetUserIdPromise = null; } } return getUserIdPromise; } /** * Ensure that the calumet access token is available and valid. Get it from the cache if it * hasn't yet expired. Otherwise refetch and cache it again. Also refetches the mandella access * token if necessary. * * @return the calumet access token */ public String ensureAccessTokens() { String refreshToken = null; String calumetAccessToken = null; String mandellaAccessToken = null; int mandellaExpirationTime = -1; int calumetExpirationTime = -1; int currentTime = (int) (System.currentTimeMillis() / 1000); AccountManager am = AccountManager.get(TomahawkApp.getContext()); if (am != null && getAccount() != null) { mandellaAccessToken = am.getUserData(getAccount(), MANDELLA_ACCESS_TOKEN_HATCHET); String mandellaExpirationTimeString = am.getUserData(getAccount(), MANDELLA_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET); if (mandellaExpirationTimeString != null) { mandellaExpirationTime = Integer.valueOf(mandellaExpirationTimeString); } calumetAccessToken = am.getUserData(getAccount(), CALUMET_ACCESS_TOKEN_HATCHET); String calumetExpirationTimeString = am.getUserData(getAccount(), CALUMET_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET); if (calumetExpirationTimeString != null) { calumetExpirationTime = Integer.valueOf(mandellaExpirationTimeString); } refreshToken = am.peekAuthToken(getAccount(), AUTH_TOKEN_HATCHET); } if (refreshToken != null && (mandellaAccessToken == null || currentTime > mandellaExpirationTime - EXPIRING_LIMIT)) { Log.d(TAG, "Mandella access token has expired, refreshing ..."); mandellaAccessToken = fetchAccessToken(RESPONSE_TOKEN_TYPE_BEARER, refreshToken); } if (mandellaAccessToken != null && (calumetAccessToken == null || currentTime > calumetExpirationTime - EXPIRING_LIMIT)) { Log.d(TAG, "Calumet access token has expired, refreshing ..."); calumetAccessToken = fetchAccessToken(RESPONSE_TOKEN_TYPE_CALUMET, mandellaAccessToken); } if (calumetAccessToken == null) { Log.d(TAG, "Calumet access token couldn't be fetched. " + "Most probably because no Hatchet account is logged in."); } return calumetAccessToken; } /** * Fetch the accessToken of the given tokenType by providing an existent token. The token is * cached and then returned. * * @param tokenType The token type ("bearer"(aka mandella) or "calumet") * @param token In the case of fetching the bearer token, this token should be the bearer * refresh token provided by the initial auth process. If the calumet access * token should be fetched, then the given token should be the bearer access * token. * @return the fetched access token */ public String fetchAccessToken(String tokenType, String token) { String accessToken = null; try { HatchetAuthResponse authResponse; if (tokenType.equals(RESPONSE_TOKEN_TYPE_BEARER)) { authResponse = mHatchetAuth.getBearerAccessToken(token, PARAMS_GRANT_TYPE_REFRESHTOKEN); } else { authResponse = mHatchetAuth.getAccessToken(RESPONSE_TOKEN_TYPE_BEARER + " " + token, tokenType); } AccountManager am = AccountManager.get(TomahawkApp.getContext()); if (am != null && getAccount() != null && authResponse.access_token != null) { int currentTime = (int) (System.currentTimeMillis() / 1000); long expirationTime = currentTime + authResponse.expires_in; accessToken = authResponse.access_token; if (VariousUtils.containsIgnoreCase(tokenType, RESPONSE_TOKEN_TYPE_BEARER)) { am.setUserData(getAccount(), MANDELLA_ACCESS_TOKEN_HATCHET, accessToken); am.setUserData(getAccount(), MANDELLA_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET, String.valueOf(expirationTime)); } else { am.setUserData(getAccount(), CALUMET_ACCESS_TOKEN_HATCHET, accessToken); am.setUserData(getAccount(), CALUMET_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET, String.valueOf(expirationTime)); } Log.d(TAG, "Access token fetched, current time: '" + currentTime + "', expiration time: '" + expirationTime + "'"); } else { onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, "Couldn't fetch access token"); } } catch (RetrofitError e) { Log.e(TAG, "fetchAccessToken: " + e.getClass() + ": " + e.getLocalizedMessage()); try { HatchetAuthResponse authResponse = (HatchetAuthResponse) e.getBodyAs(HatchetAuthResponse.class); if (authResponse != null && (authResponse.error != null || !VariousUtils.containsIgnoreCase(tokenType, authResponse.token_type))) { logout(); onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, "Please reenter your Hatchet credentials"); } } catch (RuntimeException e1) { onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, "Hatchet authentication error. Sorry, please try again later."); } } return accessToken; } /** * Get the Hatchet account from the AccountManager * * @return the account object or null if none could be found */ public static Account getAccount() { AccountManager am = AccountManager.get(TomahawkApp.getContext()); if (am != null) { Account[] accounts = am.getAccountsByType(ACCOUNT_TYPE); if (accounts != null && accounts.length > 0) { return accounts[0]; } } return null; } public static void storeUserId(String userId) { AccountManager am = AccountManager.get(TomahawkApp.getContext()); am.setUserData(getAccount(), USER_ID_HATCHET, userId); EventBus.getDefault().post(new UserLoginEvent()); } }