/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.support.v4.content.LocalBroadcastManager; import com.facebook.internal.NativeProtocol; import com.facebook.internal.Utility; import com.facebook.internal.Validate; import java.util.Date; final class AccessTokenManager { static final String ACTION_CURRENT_ACCESS_TOKEN_CHANGED = "com.facebook.sdk.ACTION_CURRENT_ACCESS_TOKEN_CHANGED"; static final String EXTRA_OLD_ACCESS_TOKEN = "com.facebook.sdk.EXTRA_OLD_ACCESS_TOKEN"; static final String EXTRA_NEW_ACCESS_TOKEN = "com.facebook.sdk.EXTRA_NEW_ACCESS_TOKEN"; static final String SHARED_PREFERENCES_NAME = "com.facebook.AccessTokenManager.SharedPreferences"; // Token extension constants private static final int TOKEN_EXTEND_THRESHOLD_SECONDS = 24 * 60 * 60; // 1 day private static final int TOKEN_EXTEND_RETRY_SECONDS = 60 * 60; // 1 hour private static volatile AccessTokenManager instance; private final LocalBroadcastManager localBroadcastManager; private final AccessTokenCache accessTokenCache; private AccessToken currentAccessToken; private TokenRefreshRequest currentTokenRefreshRequest; private Date lastAttemptedTokenExtendDate = new Date(0); AccessTokenManager(LocalBroadcastManager localBroadcastManager, AccessTokenCache accessTokenCache) { Validate.notNull(localBroadcastManager, "localBroadcastManager"); Validate.notNull(accessTokenCache, "accessTokenCache"); this.localBroadcastManager = localBroadcastManager; this.accessTokenCache = accessTokenCache; } static AccessTokenManager getInstance() { if (instance == null) { synchronized (AccessTokenManager.class) { if (instance == null) { Context applicationContext = FacebookSdk.getApplicationContext(); LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance( applicationContext); AccessTokenCache accessTokenCache = new AccessTokenCache(); instance = new AccessTokenManager(localBroadcastManager, accessTokenCache); } } } return instance; } AccessToken getCurrentAccessToken() { return currentAccessToken; } boolean loadCurrentAccessToken() { AccessToken accessToken = accessTokenCache.load(); if (accessToken != null) { setCurrentAccessToken(accessToken, false); return true; } return false; } void setCurrentAccessToken(AccessToken currentAccessToken) { setCurrentAccessToken(currentAccessToken, true); } private void setCurrentAccessToken(AccessToken currentAccessToken, boolean saveToCache) { AccessToken oldAccessToken = this.currentAccessToken; this.currentAccessToken = currentAccessToken; this.currentTokenRefreshRequest = null; this.lastAttemptedTokenExtendDate = new Date(0); if (saveToCache) { if (currentAccessToken != null) { accessTokenCache.save(currentAccessToken); } else { accessTokenCache.clear(); } } if (!Utility.areObjectsEqual(oldAccessToken, currentAccessToken)) { sendCurrentAccessTokenChangedBroadcast(oldAccessToken, currentAccessToken); } } private void sendCurrentAccessTokenChangedBroadcast(AccessToken oldAccessToken, AccessToken currentAccessToken) { Intent intent = new Intent(ACTION_CURRENT_ACCESS_TOKEN_CHANGED); intent.putExtra(EXTRA_OLD_ACCESS_TOKEN, oldAccessToken); intent.putExtra(EXTRA_NEW_ACCESS_TOKEN, currentAccessToken); localBroadcastManager.sendBroadcast(intent); } void extendAccessTokenIfNeeded() { if (!shouldExtendAccessToken()) { return; } currentTokenRefreshRequest = new TokenRefreshRequest(currentAccessToken); currentTokenRefreshRequest.bind(); } private boolean shouldExtendAccessToken() { if (currentAccessToken == null || currentTokenRefreshRequest != null) { return false; } Long now = new Date().getTime(); return currentAccessToken.getSource().canExtendToken() && now - lastAttemptedTokenExtendDate.getTime() > TOKEN_EXTEND_RETRY_SECONDS * 1000 && now - currentAccessToken.getLastRefresh().getTime() > TOKEN_EXTEND_THRESHOLD_SECONDS * 1000; } class TokenRefreshRequest implements ServiceConnection { final Messenger messageReceiver; Messenger messageSender = null; TokenRefreshRequest(AccessToken accessToken) { this.messageReceiver = new Messenger( new TokenRefreshRequestHandler(accessToken, this)); } public void bind() { Context context = FacebookSdk.getApplicationContext(); Intent intent = NativeProtocol.createTokenRefreshIntent(context); if (intent != null && context.bindService(intent, this, Context.BIND_AUTO_CREATE)) { lastAttemptedTokenExtendDate = new Date(); } else { cleanup(); } } @Override public void onServiceConnected(ComponentName className, IBinder service) { messageSender = new Messenger(service); refreshToken(); } @Override public void onServiceDisconnected(ComponentName arg) { cleanup(); try { // We returned an error so there's no point in keeping the binding open. FacebookSdk.getApplicationContext().unbindService(TokenRefreshRequest.this); } catch (IllegalArgumentException ex) { // Do nothing, the connection was already unbound } } private void cleanup() { if (currentTokenRefreshRequest == this) { currentTokenRefreshRequest = null; } } private void refreshToken() { Bundle requestData = new Bundle(); requestData.putString(AccessToken.ACCESS_TOKEN_KEY, getCurrentAccessToken().getToken()); Message request = Message.obtain(); request.setData(requestData); request.replyTo = messageReceiver; try { messageSender.send(request); } catch (RemoteException e) { cleanup(); } } } static class TokenRefreshRequestHandler extends Handler { private AccessToken accessToken; private TokenRefreshRequest tokenRefreshRequest; TokenRefreshRequestHandler( AccessToken accessToken, TokenRefreshRequest tokenRefreshRequest) { super(Looper.getMainLooper()); this.accessToken = accessToken; this.tokenRefreshRequest = tokenRefreshRequest; } @Override public void handleMessage(Message msg) { AccessToken currentAccessToken = AccessToken.getCurrentAccessToken(); if (currentAccessToken != null && currentAccessToken.equals(accessToken) && msg.getData().getString(AccessToken.ACCESS_TOKEN_KEY) != null) { AccessToken newToken = AccessToken.createFromRefresh(accessToken, msg.getData()); AccessToken.setCurrentAccessToken(newToken); } // The refreshToken function should be called rarely, so there is no point in keeping // the binding open. FacebookSdk.getApplicationContext().unbindService(tokenRefreshRequest); tokenRefreshRequest.cleanup(); } } }