/* * Copyright (C) 2015 Simon Vig Therkildsen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.simonvt.cathode.module; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.text.format.DateUtils; import java.io.IOException; import javax.inject.Inject; import net.simonvt.cathode.BuildConfig; import net.simonvt.cathode.CathodeApp; import net.simonvt.cathode.api.TraktSettings; import net.simonvt.cathode.api.entity.AccessToken; import net.simonvt.cathode.api.entity.TokenRequest; import net.simonvt.cathode.api.enumeration.GrantType; import net.simonvt.cathode.api.service.AuthorizationService; import net.simonvt.cathode.settings.Settings; import retrofit2.Call; import retrofit2.Response; import timber.log.Timber; public class ApiSettings implements TraktSettings { private static volatile ApiSettings instance; public static ApiSettings getInstance(Context context) { if (instance == null) { synchronized (ApiSettings.class) { if (instance == null) { instance = new ApiSettings(context); } } } return instance; } private Context context; private SharedPreferences settings; @Inject AuthorizationService authService; private volatile boolean refreshingToken; public ApiSettings(Context context) { this.context = context; settings = PreferenceManager.getDefaultSharedPreferences(context); } @Override public String getAccessToken() { synchronized (this) { return settings.getString(Settings.TRAKT_ACCESS_TOKEN, null); } } @Override public String getRefreshToken() { synchronized (this) { return settings.getString(Settings.TRAKT_REFRESH_TOKEN, null); } } private boolean isTokenExpired() { synchronized (this) { return settings.getLong(Settings.TRAKT_TOKEN_EXPIRATION, 0) < System.currentTimeMillis(); } } private boolean isRefreshingToken() { synchronized (this) { return refreshingToken; } } public void setRefreshingToken(boolean refreshingToken) { synchronized (this) { this.refreshingToken = refreshingToken; } } @Override public void updateTokens(AccessToken tokens) { synchronized (this) { final long expirationMillis = System.currentTimeMillis() + (tokens.getExpiresIn() * DateUtils.SECOND_IN_MILLIS); settings.edit() .putString(Settings.TRAKT_ACCESS_TOKEN, tokens.getAccessToken()) .putString(Settings.TRAKT_REFRESH_TOKEN, tokens.getRefreshToken()) .putLong(Settings.TRAKT_TOKEN_EXPIRATION, expirationMillis) .apply(); } } @Override public String getClientId() { return BuildConfig.TRAKT_CLIENT_ID; } @Override public String getSecret() { return BuildConfig.TRAKT_SECRET; } @Override public String getRedirectUrl() { return BuildConfig.TRAKT_REDIRECT_URL; } private void clearRefreshToken() { synchronized (this) { settings.edit().remove(Settings.TRAKT_REFRESH_TOKEN).apply(); } } @Override public String refreshToken() { synchronized (this) { if (!isRefreshingToken()) { if (authService == null) { CathodeApp.inject(context, this); } if (!isTokenExpired()) { Timber.d("Token still valid"); return getAccessToken(); } final String refreshToken = getRefreshToken(); if (refreshToken == null) { return null; } try { setRefreshingToken(true); TokenRequest tokenRequest = TokenRequest.refreshToken(refreshToken, getClientId(), getSecret(), getRedirectUrl(), GrantType.REFRESH_TOKEN); Timber.d("Getting new tokens, with refresh token: %s", refreshToken); Call<AccessToken> call = authService.getToken(tokenRequest); Response<AccessToken> response = call.execute(); if (response.isSuccessful()) { AccessToken token = response.body(); updateTokens(token); return token.getAccessToken(); } else { if (response.code() == 401) { clearRefreshToken(); return null; } String message = "Code: " + response.code(); Timber.e(new TokenRefreshFailedException(message), "Unable to get token"); } } catch (IOException e) { Timber.d(e, "Unable to get new tokens"); } finally { setRefreshingToken(false); } } } return null; } public static class TokenRefreshFailedException extends Exception { public TokenRefreshFailedException() { super(); } public TokenRefreshFailedException(String detailMessage) { super(detailMessage); } public TokenRefreshFailedException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } public TokenRefreshFailedException(Throwable throwable) { super(throwable); } } }