SalesforceSDKManager.java
/*
* Copyright (c) 2014-present, salesforce.com, inc.
* All rights reserved.
* Redistribution and use of this software in source and binary forms, with or
* without modification, are permitted provided that the following conditions
* are met:
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of salesforce.com, inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission of salesforce.com, inc.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.salesforce.androidsdk.app;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Build;
import android.os.SystemClock;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import com.salesforce.androidsdk.accounts.UserAccount;
import com.salesforce.androidsdk.accounts.UserAccountManager;
import com.salesforce.androidsdk.analytics.EventBuilderHelper;
import com.salesforce.androidsdk.analytics.SalesforceAnalyticsManager;
import com.salesforce.androidsdk.analytics.security.Encryptor;
import com.salesforce.androidsdk.auth.AuthenticatorService;
import com.salesforce.androidsdk.auth.HttpAccess;
import com.salesforce.androidsdk.auth.OAuth2;
import com.salesforce.androidsdk.config.AdminPermsManager;
import com.salesforce.androidsdk.config.AdminSettingsManager;
import com.salesforce.androidsdk.config.BootConfig;
import com.salesforce.androidsdk.config.LoginServerManager;
import com.salesforce.androidsdk.push.PushMessaging;
import com.salesforce.androidsdk.push.PushNotificationInterface;
import com.salesforce.androidsdk.rest.ClientManager;
import com.salesforce.androidsdk.rest.ClientManager.LoginOptions;
import com.salesforce.androidsdk.security.PasscodeManager;
import com.salesforce.androidsdk.ui.AccountSwitcherActivity;
import com.salesforce.androidsdk.ui.LoginActivity;
import com.salesforce.androidsdk.ui.PasscodeActivity;
import com.salesforce.androidsdk.ui.SalesforceR;
import com.salesforce.androidsdk.util.EventsObservable;
import com.salesforce.androidsdk.util.EventsObservable.EventType;
import java.net.URI;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* This class serves as an interface to the various
* functions of the Salesforce SDK. In order to use the SDK,
* your app must first instantiate the singleton SalesforceSDKManager
* object by calling the static init() method. After calling init(),
* use the static getInstance() method to access the
* singleton SalesforceSDKManager object.
*/
@SuppressWarnings("deprecation")
public class SalesforceSDKManager {
/**
* Current version of this SDK.
*/
public static final String SDK_VERSION = "5.1.0.dev";
/**
* Intent action that specifies that logout was completed.
*/
public static final String LOGOUT_COMPLETE_INTENT_ACTION = "com.salesforce.LOGOUT_COMPLETE";
/**
* Default app name.
*/
private static final String DEFAULT_APP_DISPLAY_NAME = "Salesforce";
private static final String TAG = "SalesforceSDKManager";
/**
* Instance of the SalesforceSDKManager to use for this process.
*/
protected static SalesforceSDKManager INSTANCE;
/**
* Timeout value for push un-registration.
*/
private static final int PUSH_UNREGISTER_TIMEOUT_MILLIS = 30000;
private static final String FEATURE_PUSH_NOTIFICATIONS = "PN";
protected Context context;
protected KeyInterface keyImpl;
protected LoginOptions loginOptions;
protected Class<? extends Activity> mainActivityClass;
protected Class<? extends Activity> loginActivityClass = LoginActivity.class;
protected Class<? extends PasscodeActivity> passcodeActivityClass = PasscodeActivity.class;
protected Class<? extends AccountSwitcherActivity> switcherActivityClass = AccountSwitcherActivity.class;
private String encryptionKey;
private SalesforceR salesforceR = new SalesforceR();
private PasscodeManager passcodeManager;
private LoginServerManager loginServerManager;
private boolean isTestRun = false;
private boolean isLoggingOut = false;
private AdminSettingsManager adminSettingsManager;
private AdminPermsManager adminPermsManager;
private PushNotificationInterface pushNotificationInterface;
private String uid; // device id
private volatile boolean loggedOut = false;
private SortedSet<String> features;
private List<String> additionalOauthKeys;
/**
* PasscodeManager object lock.
*/
private Object passcodeManagerLock = new Object();
/**
* Returns a singleton instance of this class.
*
* @return Singleton instance of SalesforceSDKManager.
*/
public static SalesforceSDKManager getInstance() {
if (INSTANCE != null) {
return INSTANCE;
} else {
throw new RuntimeException("Applications need to call SalesforceSDKManager.init() first.");
}
}
/**
*
* @return true if SalesforceSDKManager has been initialized already
*/
public static boolean hasInstance() {
return INSTANCE != null;
}
/**
* Protected constructor.
* @param context Application context.
* @param keyImpl Implementation for KeyInterface.
* @param mainActivity Activity that should be launched after the login flow.
* @param loginActivity Login activity.
*/
protected SalesforceSDKManager(Context context, KeyInterface keyImpl,
Class<? extends Activity> mainActivity, Class<? extends Activity> loginActivity) {
this.uid = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
this.context = context;
this.keyImpl = keyImpl;
this.mainActivityClass = mainActivity;
if (loginActivity != null) {
this.loginActivityClass = loginActivity;
}
this.features = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
}
/**
* Returns the class for the main activity.
*
* @return The class for the main activity.
*/
public Class<? extends Activity> getMainActivityClass() {
return mainActivityClass;
}
/**
* Returns the class for the account switcher activity.
*
* @return The class for the account switcher activity.
*/
public Class<? extends AccountSwitcherActivity> getAccountSwitcherActivityClass() {
return switcherActivityClass;
}
/**
* Returns the class for the account switcher activity.
*
* @return The class for the account switcher activity.
*/
public void setAccountSwitcherActivityClass(Class<? extends AccountSwitcherActivity> activity) {
if (activity != null) {
switcherActivityClass = activity;
}
}
public interface KeyInterface {
/**
* Defines a single function for retrieving the key
* associated with a given name.
*
* For the given name, this function must return the same key
* even when the application is restarted. The value this
* function returns must be Base64 encoded.
*
* {@link Encryptor#isBase64Encoded(String)} can be used to
* determine whether the generated key is Base64 encoded.
*
* {@link Encryptor#hash(String, String)} can be used to
* generate a Base64 encoded string.
*
* For example:
* <code>
* Encryptor.hash(name + "12s9adfgret=6235inkasd=012", name + "12kl0dsakj4-cuygsdf625wkjasdol8");
* </code>
*
* @param name The name associated with the key.
* @return The key used for encrypting salts and keys.
*/
public String getKey(String name);
}
/**
* For the given name, this function must return the same key
* even when the application is restarted. The value this
* function returns must be Base64 encoded.
*
* {@link Encryptor#isBase64Encoded(String)} can be used to
* determine whether the generated key is Base64 encoded.
*
* {@link Encryptor#hash(String, String)} can be used to
* generate a Base64 encoded string.
*
* For example:
* <code>
* Encryptor.hash(name + "12s9adfgret=6235inkasd=012", name + "12kl0dsakj4-cuygsdf625wkjasdol8");
* </code>
*
* @param name The name associated with the key.
* @return The key used for encrypting salts and keys.
*/
public String getKey(String name) {
String key = null;
if (keyImpl != null) {
key = keyImpl.getKey(name);
}
return key;
}
/**
* Before Mobile SDK 1.3, SalesforceSDK was packaged as a jar, and each project had to provide
* a subclass of SalesforceR.
*
* Since 1.3, SalesforceSDK is packaged as a library project, so the SalesforceR subclass is no longer needed.
* @return SalesforceR object which allows reference to resources living outside the SDK.
*/
public SalesforceR getSalesforceR() {
return salesforceR;
}
/**
* Returns the class of the activity used to perform the login process and create the account.
*
* @return the class of the activity used to perform the login process and create the account.
*/
public Class<? extends Activity> getLoginActivityClass() {
return loginActivityClass;
}
/**
* Returns unique device ID.
*
* @return Device ID.
*/
public String getDeviceId() {
return uid;
}
/**
* Returns login options associated with the app.
*
* @return LoginOptions instance.
*/
public LoginOptions getLoginOptions() {
return getLoginOptions(null, null);
}
public LoginOptions getLoginOptions(String jwt, String url) {
if (loginOptions == null) {
final BootConfig config = BootConfig.getBootConfig(context);
if (TextUtils.isEmpty(jwt)) {
loginOptions = new LoginOptions(url, getPasscodeHash(), config.getOauthRedirectURI(),
config.getRemoteAccessConsumerKey(), config.getOauthScopes(), null);
} else {
loginOptions = new LoginOptions(url, getPasscodeHash(), config.getOauthRedirectURI(),
config.getRemoteAccessConsumerKey(), config.getOauthScopes(), null, jwt);
}
} else {
loginOptions.setJwt(jwt);
loginOptions.setUrl(url);
}
return loginOptions;
}
/**
* For internal use only. Initializes required components.
* @param context Application context.
* @param keyImpl Implementation of KeyInterface.
* @param mainActivity Activity to be launched after the login flow.
* @param loginActivity Login activity.
*/
private static void init(Context context, KeyInterface keyImpl,
Class<? extends Activity> mainActivity, Class<? extends Activity> loginActivity) {
if (INSTANCE == null) {
INSTANCE = new SalesforceSDKManager(context, keyImpl, mainActivity, loginActivity);
}
initInternal(context);
EventsObservable.get().notifyEvent(EventType.AppCreateComplete);
}
/**
* For internal use by Salesforce Mobile SDK or by subclasses
* of SalesforceSDKManager. Initializes required components.
*
* @param context Application context.
*/
public static void initInternal(Context context) {
// Initializes the encryption module.
Encryptor.init(context);
// Initializes the HTTP client.
HttpAccess.init(context, INSTANCE.getUserAgent());
// Upgrades to the latest version.
SalesforceSDKUpgradeManager.getInstance().upgrade();
}
/**
* Initializes required components. Native apps must call one overload of
* this method before using the Salesforce Mobile SDK.
*
* @param context Application context.
* @param keyImpl Implementation of KeyInterface.
* @param mainActivity Activity that should be launched after the login flow.
*/
public static void initNative(Context context, KeyInterface keyImpl, Class<? extends Activity> mainActivity) {
SalesforceSDKManager.init(context, keyImpl, mainActivity, LoginActivity.class);
}
/**
* Initializes required components. Native apps must call one overload of
* this method before using the Salesforce Mobile SDK.
*
* @param context Application context.
* @param keyImpl Implementation of KeyInterface.
* @param mainActivity Activity that should be launched after the login flow.
* @param loginActivity Login activity.
*/
public static void initNative(Context context, KeyInterface keyImpl,
Class<? extends Activity> mainActivity, Class<? extends Activity> loginActivity) {
SalesforceSDKManager.init(context, keyImpl, mainActivity, loginActivity);
}
/**
* Sets a custom passcode activity class to be used instead of the default class.
* The custom class must subclass PasscodeActivity.
*
* @param activity Subclass of PasscodeActivity.
*/
public void setPasscodeActivity(Class<? extends PasscodeActivity> activity) {
if (activity != null) {
passcodeActivityClass = activity;
}
}
/**
* Returns the descriptor of the passcode activity class that's currently in use.
*
* @return Passcode activity class descriptor.
*/
public Class<? extends PasscodeActivity> getPasscodeActivity() {
return passcodeActivityClass;
}
/**
* Indicates whether the SDK should automatically log out when the
* access token is revoked. If you override this method to return
* false, your app is responsible for handling its own cleanup when the
* access token is revoked.
*
* @return True if the SDK should automatically logout.
*/
public boolean shouldLogoutWhenTokenRevoked() {
return true;
}
/**
* Returns the application context.
*
* @return Application context.
*/
public Context getAppContext() {
return context;
}
/**
* Returns the login server manager associated with SalesforceSDKManager.
*
* @return LoginServerManager instance.
*/
public synchronized LoginServerManager getLoginServerManager() {
if (loginServerManager == null) {
loginServerManager = new LoginServerManager(context);
}
return loginServerManager;
}
/**
* Sets a receiver that handles received push notifications.
*
* @param pnInterface Implementation of PushNotificationInterface.
*/
public synchronized void setPushNotificationReceiver(PushNotificationInterface pnInterface) {
this.registerUsedAppFeature(FEATURE_PUSH_NOTIFICATIONS);
pushNotificationInterface = pnInterface;
}
/**
* Returns the receiver that's configured to handle incoming push notifications.
*
* @return Configured implementation of PushNotificationInterface.
*/
public synchronized PushNotificationInterface getPushNotificationReceiver() {
return pushNotificationInterface;
}
/**
* Returns the passcode manager that's associated with SalesforceSDKManager.
*
* @return PasscodeManager instance.
*/
public PasscodeManager getPasscodeManager() {
synchronized (passcodeManagerLock) {
if (passcodeManager == null) {
passcodeManager = new PasscodeManager(context);
}
return passcodeManager;
}
}
/**
* Returns the user account manager that's associated with SalesforceSDKManager.
*
* @return UserAccountManager instance.
*/
public UserAccountManager getUserAccountManager() {
return UserAccountManager.getInstance();
}
/**
* Returns the administrator settings manager that's associated with SalesforceSDKManager.
*
* @return AdminSettingsManager instance.
*/
public synchronized AdminSettingsManager getAdminSettingsManager() {
if (adminSettingsManager == null) {
adminSettingsManager = new AdminSettingsManager();
}
return adminSettingsManager;
}
/**
* Returns the administrator permissions manager that's associated with SalesforceSDKManager.
*
* @return AdminPermsManager instance.
*/
public synchronized AdminPermsManager getAdminPermsManager() {
if (adminPermsManager == null) {
adminPermsManager = new AdminPermsManager();
}
return adminPermsManager;
}
/**
* Changes the passcode to a new value.
*
* @param oldPass Old passcode.
* @param newPass New passcode.
*/
public synchronized void changePasscode(String oldPass, String newPass) {
if (!isNewPasscode(oldPass, newPass)) {
return;
}
// Resets the cached encryption key, since the passcode has changed.
encryptionKey = null;
SalesforceAnalyticsManager.changePasscode(oldPass, newPass);
ClientManager.changePasscode(oldPass, newPass);
}
/**
* Indicates whether the new passcode is different from the old passcode.
*
* @param oldPass Old passcode.
* @param newPass New passcode.
* @return True if the new passcode is different from the old passcode.
*/
protected boolean isNewPasscode(String oldPass, String newPass) {
return !((oldPass == null && newPass == null)
|| (oldPass != null && newPass != null && oldPass.trim().equals(newPass.trim())));
}
/**
* Returns the encryption key being used.
*
* @param actualPass Passcode.
* @return Encryption key for passcode.
*/
public synchronized String getEncryptionKeyForPasscode(String actualPass) {
if (actualPass != null && !actualPass.trim().equals("")) {
return actualPass;
}
if (encryptionKey == null) {
encryptionKey = getPasscodeManager().hashForEncryption("");
}
return encryptionKey;
}
/**
* Returns the app display name used by the passcode dialog.
*
* @return App display string.
*/
public String getAppDisplayString() {
return DEFAULT_APP_DISPLAY_NAME;
}
/**
* Returns the passcode hash being used.
*
* @return The hashed passcode, or null if it's not required.
*/
public String getPasscodeHash() {
return getPasscodeManager().getPasscodeHash();
}
/**
* Returns the name of the application (as defined in AndroidManifest.xml).
*
* @return The name of the application.
*/
public String getApplicationName() {
return context.getPackageManager().getApplicationLabel(context.getApplicationInfo()).toString();
}
/**
* Checks if network connectivity exists.
*
* @return True if a network connection is available.
*/
public boolean hasNetwork() {
return HttpAccess.DEFAULT.hasNetwork();
}
/**
* Adds an additional set of OAuth keys to fetch and store from the token endpoint.
*
* @param additionalOauthKeys List of additional OAuth keys.
*/
public void setAdditionalOauthKeys(List<String> additionalOauthKeys) {
this.additionalOauthKeys = additionalOauthKeys;
}
/**
* Returns the list of additional OAuth keys set for this application.
*
* @return List of additional OAuth keys.
*/
public List<String> getAdditionalOauthKeys() {
return additionalOauthKeys;
}
/**
* Cleans up cached credentials and data.
*
* @param frontActivity Front activity.
* @param account Account.
*/
protected void cleanUp(Activity frontActivity, Account account) {
final UserAccount userAccount = UserAccountManager.getInstance().buildUserAccount(account);
SalesforceAnalyticsManager.reset(userAccount);
final List<UserAccount> users = getUserAccountManager().getAuthenticatedUsers();
// Finishes front activity if specified, and if this is the last account.
if (frontActivity != null && (users == null || users.size() <= 1)) {
frontActivity.finish();
}
/*
* Checks how many accounts are left that are authenticated. If only one
* account is left, this is the account that is being removed. In this
* case, we can safely reset passcode manager, admin prefs, and encryption keys.
* Otherwise, we don't reset passcode manager and admin prefs since
* there might be other accounts on that same org, and these policies
* are stored at the org level.
*/
if (users == null || users.size() <= 1) {
getAdminSettingsManager().resetAll();
getAdminPermsManager().resetAll();
adminSettingsManager = null;
adminPermsManager = null;
getPasscodeManager().reset(context);
passcodeManager = null;
encryptionKey = null;
UUIDManager.resetUuids();
}
}
/**
* Starts login flow if user account has been removed.
*/
protected void startLoginPage() {
// Clears cookies.
removeAllCookies();
// Restarts the application.
final Intent i = new Intent(context, getMainActivityClass());
i.setPackage(getAppContext().getPackageName());
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
}
/**
* Starts account switcher activity if an account has been removed.
*/
public void startSwitcherActivityIfRequired() {
// Clears cookies.
removeAllCookies();
/*
* If the number of accounts remaining is 0, shows the login page.
* If the number of accounts remaining is 1, switches to that user
* automatically. If there is more than 1 account logged in, shows
* the account switcher screen, so that the user can pick which
* account to switch to.
*/
final UserAccountManager userAccMgr = getUserAccountManager();
final List<UserAccount> accounts = userAccMgr.getAuthenticatedUsers();
if (accounts == null || accounts.size() == 0) {
startLoginPage();
} else if (accounts.size() == 1) {
userAccMgr.switchToUser(accounts.get(0));
} else {
final Intent i = new Intent(context, switcherActivityClass);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
}
}
/**
* Unregisters from push notifications for both GCM (Android) and SFDC, and waits either for
* unregistration to complete or for the operation to time out. The timeout period is defined
* in PUSH_UNREGISTER_TIMEOUT_MILLIS.
*
* If timeout occurs while the user is logged in, this method attempts to unregister the push
* unregistration receiver, and then removes the user's account.
*
* @param clientMgr ClientManager instance.
* @param showLoginPage True - if the login page should be shown, False - otherwise.
* @param refreshToken Refresh token.
* @param clientId Client ID.
* @param loginServer Login server.
* @param account Account instance.
* @param frontActivity Front activity.
*/
private void unregisterPush(final ClientManager clientMgr, final boolean showLoginPage,
final String refreshToken, final String clientId,
final String loginServer, final Account account, final Activity frontActivity) {
final IntentFilter intentFilter = new IntentFilter(PushMessaging.UNREGISTERED_ATTEMPT_COMPLETE_EVENT);
final BroadcastReceiver pushUnregisterReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(PushMessaging.UNREGISTERED_ATTEMPT_COMPLETE_EVENT)) {
postPushUnregister(this, clientMgr, showLoginPage,
refreshToken, clientId, loginServer, account, frontActivity);
}
}
};
getAppContext().registerReceiver(pushUnregisterReceiver, intentFilter);
// Unregisters from notifications on logout.
final UserAccount userAcc = getUserAccountManager().buildUserAccount(account);
PushMessaging.unregister(context, userAcc);
/*
* Starts a background thread to wait up to the timeout period. If
* another thread has already performed logout, we exit immediately.
*/
(new Thread() {
public void run() {
long startTime = System.currentTimeMillis();
while ((System.currentTimeMillis() - startTime) < PUSH_UNREGISTER_TIMEOUT_MILLIS && !loggedOut) {
// Waits for half a second at a time.
SystemClock.sleep(500);
}
postPushUnregister(pushUnregisterReceiver, clientMgr, showLoginPage,
refreshToken, clientId, loginServer, account, frontActivity);
};
}).start();
}
/**
* This method is called either when unregistration for push notifications
* is complete and the user has logged out, or when a timeout occurs while waiting.
* If the user has not logged out, this method attempts to unregister the push
* notification unregistration receiver, and then removes the user's account.
*
* @param pushReceiver Broadcast receiver.
* @param clientMgr ClientManager instance.
* @param showLoginPage True - if the login page should be shown, False - otherwise.
* @param refreshToken Refresh token.
* @param clientId Client ID.
* @param loginServer Login server.
* @param account Account instance.
* @param frontActivity Front activity.
*/
private synchronized void postPushUnregister(BroadcastReceiver pushReceiver,
final ClientManager clientMgr, final boolean showLoginPage,
final String refreshToken, final String clientId,
final String loginServer, final Account account, Activity frontActivity) {
if (!loggedOut) {
try {
context.unregisterReceiver(pushReceiver);
} catch (Exception e) {
Log.e("SalesforceSDKManager", "Exception occurred while un-registering.", e);
}
removeAccount(clientMgr, showLoginPage, refreshToken, clientId, loginServer, account, frontActivity);
}
}
/**
* Destroys the stored authentication credentials (removes the account).
*
* @param frontActivity Front activity.
*/
public void logout(Activity frontActivity) {
logout(frontActivity, true);
}
/**
* Destroys the stored authentication credentials (removes the account).
*
* @param account Account.
* @param frontActivity Front activity.
*/
public void logout(Account account, Activity frontActivity) {
logout(account, frontActivity, true);
}
/**
* Destroys the stored authentication credentials (removes the account)
* and, if requested, restarts the app.
*
* @param frontActivity Front activity.
* @param showLoginPage If true, displays the login page after removing the account.
*/
public void logout(Activity frontActivity, final boolean showLoginPage) {
final ClientManager clientMgr = new ClientManager(context, getAccountType(),
null, shouldLogoutWhenTokenRevoked());
final Account account = clientMgr.getAccount();
logout(account, frontActivity, showLoginPage);
}
/**
* Destroys the stored authentication credentials (removes the account)
* and, if requested, restarts the app.
*
* @param account Account.
* @param frontActivity Front activity.
* @param showLoginPage If true, displays the login page after removing the account.
*/
public void logout(Account account, Activity frontActivity, final boolean showLoginPage) {
EventBuilderHelper.createAndStoreEvent("userLogout", null, TAG, null);
final ClientManager clientMgr = new ClientManager(context, getAccountType(),
null, shouldLogoutWhenTokenRevoked());
isLoggingOut = true;
final AccountManager mgr = AccountManager.get(context);
String refreshToken = null;
String clientId = null;
String loginServer = null;
if (account != null) {
String passcodeHash = getPasscodeHash();
refreshToken = SalesforceSDKManager.decryptWithPasscode(mgr.getPassword(account),
passcodeHash);
clientId = SalesforceSDKManager.decryptWithPasscode(mgr.getUserData(account,
AuthenticatorService.KEY_CLIENT_ID), passcodeHash);
loginServer = SalesforceSDKManager.decryptWithPasscode(mgr.getUserData(account,
AuthenticatorService.KEY_INSTANCE_URL), passcodeHash);
}
/*
* Makes a call to un-register from push notifications, only
* if the refresh token is available.
*/
final UserAccount userAcc = getUserAccountManager().buildUserAccount(account);
if (PushMessaging.isRegistered(context, userAcc) && refreshToken != null) {
loggedOut = false;
unregisterPush(clientMgr, showLoginPage, refreshToken, clientId,
loginServer, account, frontActivity);
} else {
removeAccount(clientMgr, showLoginPage, refreshToken, clientId,
loginServer, account, frontActivity);
}
}
/**
* Removes the account upon logout.
*
* @param clientMgr ClientManager instance.
* @param showLoginPage If true, displays the login page after removing the account.
* @param refreshToken Refresh token.
* @param clientId Client ID.
* @param loginServer Login server.
* @param account Account instance.
* @param frontActivity Front activity.
*/
private void removeAccount(ClientManager clientMgr, final boolean showLoginPage,
String refreshToken, String clientId, String loginServer,
Account account, Activity frontActivity) {
loggedOut = true;
cleanUp(frontActivity, account);
/*
* Removes the existing account, if any. 'account == null' does not
* guarantee that there are no accounts to remove. In the 'Forgot Passcode'
* flow there could be accounts to remove, but we don't have them, since
* we don't have the passcode hash to decrypt them. Hence, we query
* AccountManager directly here and remove the accounts for the case
* where 'account == null'. If AccountManager doesn't have accounts
* either, then there's nothing to do.
*/
if (account == null) {
final AccountManager accMgr = AccountManager.get(context);
if (accMgr != null) {
final Account[] accounts = accMgr.getAccountsByType(getAccountType());
if (accounts.length > 0) {
for (int i = 0; i < accounts.length - 1; i++) {
clientMgr.removeAccounts(accounts);
}
clientMgr.removeAccountAsync(accounts[accounts.length - 1],
new AccountManagerCallback<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> arg0) {
notifyLogoutComplete(showLoginPage);
}
});
} else {
notifyLogoutComplete(showLoginPage);
}
} else {
notifyLogoutComplete(showLoginPage);
}
} else {
clientMgr.removeAccountAsync(account, new AccountManagerCallback<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> arg0) {
notifyLogoutComplete(showLoginPage);
}
});
}
isLoggingOut = false;
// Revokes the existing refresh token.
if (shouldLogoutWhenTokenRevoked() && account != null && refreshToken != null) {
new RevokeTokenTask(refreshToken, clientId, loginServer).execute();
}
}
private void notifyLogoutComplete(boolean showLoginPage) {
EventsObservable.get().notifyEvent(EventType.LogoutComplete);
sendLogoutCompleteIntent();
if (showLoginPage) {
startSwitcherActivityIfRequired();
}
}
/**
* Returns a user agent string based on the Mobile SDK version. The user agent takes the following form:
* SalesforceMobileSDK/{salesforceSDK version} android/{android OS version} appName/appVersion {Native|Hybrid} uid_{device id}
*
* @return The user agent string to use for all requests.
*/
public final String getUserAgent() {
return getUserAgent("");
}
public String getUserAgent(String qualifier) {
String appName = "";
String appVersion = "";
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
appName = context.getString(packageInfo.applicationInfo.labelRes);
appVersion = packageInfo.versionName;
} catch (NameNotFoundException e) {
Log.w("SalesforceSDKManager", e);
} catch (Resources.NotFoundException nfe) {
// A test harness such as Gradle does NOT have an application name.
Log.w("SalesforceSDKManager", nfe);
}
String appTypeWithQualifier = getAppType() + qualifier;
return String.format("SalesforceMobileSDK/%s android mobile/%s (%s) %s/%s %s uid_%s ftr_%s",
SDK_VERSION, Build.VERSION.RELEASE, Build.MODEL, appName, appVersion, appTypeWithQualifier, uid, TextUtils.join(".",features));
}
/**
* Adds AppFeature code to User Agent header for reporting.
*/
public void registerUsedAppFeature(String appFeatureCode) {
features.add(appFeatureCode);
}
/**
* Removed AppFeature code to User Agent header for reporting.
*/
public void unregisterUsedAppFeature(String appFeatureCode) {
features.remove(appFeatureCode);
}
/**
* @return app type as String
*/
public String getAppType() {
return "Native";
}
/**
* Indicates whether the application is a hybrid application.
*
* @return True if this is a hybrid application.
*/
public boolean isHybrid() {
return false;
}
/**
* Returns the authentication account type (which should match authenticator.xml).
*
* @return Account type string.
*/
public String getAccountType() {
return context.getString(getSalesforceR().stringAccountType());
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(this.getClass()).append(": {\n")
.append(" accountType: ").append(getAccountType()).append("\n")
.append(" userAgent: ").append(getUserAgent()).append("\n")
.append(" mainActivityClass: ").append(getMainActivityClass()).append("\n")
.append(" isFileSystemEncrypted: ").append(Encryptor.isFileSystemEncrypted()).append("\n");
if (passcodeManager != null) {
// passcodeManager may be null at startup if the app is running in debug mode.
sb.append(" hasStoredPasscode: ").append(passcodeManager.hasStoredPasscode(context)).append("\n");
}
sb.append("}\n");
return sb.toString();
}
/**
* Encrypts the given data using the given passcode as the encryption key.
*
* @param data Data to be encrypted.
* @param passcode Encryption key.
* @return Encrypted data.
*/
public static String encryptWithPasscode(String data, String passcode) {
return Encryptor.encrypt(data, SalesforceSDKManager.INSTANCE.getEncryptionKeyForPasscode(passcode));
}
/**
* Decrypts the given data using the given passcode as the decryption key.
*
* @param data Data to be decrypted.
* @param passcode Decryption key.
* @return Decrypted data.
*/
public static String decryptWithPasscode(String data, String passcode) {
return Encryptor.decrypt(data, SalesforceSDKManager.INSTANCE.getEncryptionKeyForPasscode(passcode));
}
/**
* Asynchronous task for revoking the refresh token on logout.
*
* @author bhariharan
*/
private class RevokeTokenTask extends AsyncTask<Void, Void, Void> {
private String refreshToken;
private String clientId;
private String loginServer;
public RevokeTokenTask(String refreshToken, String clientId, String loginServer) {
this.refreshToken = refreshToken;
this.clientId = clientId;
this.loginServer = loginServer;
}
@Override
protected Void doInBackground(Void... nothings) {
try {
OAuth2.revokeRefreshToken(HttpAccess.DEFAULT, new URI(loginServer), refreshToken);
} catch (Exception e) {
Log.w("SalesforceSDKManager", e);
}
return null;
}
}
/**
* Retrieves a property value that indicates whether the current run is a test run.
*
* @return True if the current run is a test run.
*/
public boolean getIsTestRun() {
return INSTANCE.isTestRun;
}
/**
* Sets a property that indicates whether the current run is a test run.
*
* @param isTestRun True if the current run is a test run.
*/
public void setIsTestRun(boolean isTestRun) {
INSTANCE.isTestRun = isTestRun;
}
/**
* Retrieves a property value that indicates whether logout is in progress.
*
* @return True if logout is in progress.
*/
public boolean isLoggingOut() {
return isLoggingOut;
}
/**
* @return ClientManager
*/
public ClientManager getClientManager() {
return new ClientManager(getAppContext(), getAccountType(), getLoginOptions(), true);
}
/**
* @return ClientManager
*/
public ClientManager getClientManager(String jwt, String url) {
return new ClientManager(getAppContext(), getAccountType(), getLoginOptions(jwt, url), true);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void removeAllCookies() {
/*
* TODO: Remove this conditional once 'minApi >= 21'.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().removeAllCookies(null);
} else {
CookieSyncManager.createInstance(context);
CookieManager.getInstance().removeAllCookie();
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void removeSessionCookies() {
/*
* TODO: Remove this conditional once 'minApi >= 21'.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().removeSessionCookies(null);
} else {
CookieSyncManager.createInstance(context);
CookieManager.getInstance().removeSessionCookie();
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void syncCookies() {
/*
* TODO: Remove this conditional once 'minApi >= 21'.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().flush();
} else {
CookieSyncManager.createInstance(context);
CookieSyncManager.getInstance().sync();
}
}
private void sendLogoutCompleteIntent() {
final Intent intent = new Intent(LOGOUT_COMPLETE_INTENT_ACTION);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent);
}
}