ParseApplication/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUser.java

1550 lines
53 KiB
Java

/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.os.Bundle;
import android.os.Parcel;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import bolts.Continuation;
import bolts.Task;
/**
* The {@code ParseUser} is a local representation of user data that can be saved and retrieved from
* the Parse cloud.
*/
@ParseClassName("_User")
public class ParseUser extends ParseObject {
private static final String KEY_SESSION_TOKEN = "sessionToken";
private static final String KEY_AUTH_DATA = "authData";
private static final String KEY_USERNAME = "username";
private static final String KEY_PASSWORD = "password";
private static final String KEY_EMAIL = "email";
private static final List<String> READ_ONLY_KEYS = Collections.unmodifiableList(
Arrays.asList(KEY_SESSION_TOKEN, KEY_AUTH_DATA));
private static final String PARCEL_KEY_IS_CURRENT_USER = "_isCurrentUser";
/**
* Constructs a query for {@code ParseUser}.
*
* @see com.parse.ParseQuery#getQuery(Class)
*/
public static ParseQuery<ParseUser> getQuery() {
return ParseQuery.getQuery(ParseUser.class);
}
/* package for tests */ static ParseUserController getUserController() {
return ParseCorePlugins.getInstance().getUserController();
}
/* package for tests */ static ParseCurrentUserController getCurrentUserController() {
return ParseCorePlugins.getInstance().getCurrentUserController();
}
/* package for tests */ static ParseAuthenticationManager getAuthenticationManager() {
return ParseCorePlugins.getInstance().getAuthenticationManager();
}
/** package */ static class State extends ParseObject.State {
/** package */ static class Builder extends Init<Builder> {
private boolean isNew;
public Builder() {
super("_User");
}
/* package */ Builder(State state) {
super(state);
isNew = state.isNew();
}
@Override
/* package */ Builder self() {
return this;
}
@SuppressWarnings("unchecked")
@Override
public State build() {
return new State(this);
}
@Override
public Builder apply(ParseObject.State other) {
isNew(((State) other).isNew());
return super.apply(other);
}
public Builder sessionToken(String sessionToken) {
return put(KEY_SESSION_TOKEN, sessionToken);
}
public Builder authData(Map<String, Map<String, String>> authData) {
return put(KEY_AUTH_DATA, authData);
}
@SuppressWarnings("unchecked")
public Builder putAuthData(String authType, Map<String, String> authData) {
Map<String, Map<String, String>> newAuthData =
(Map<String, Map<String, String>>) serverData.get(KEY_AUTH_DATA);
if (newAuthData == null) {
newAuthData = new HashMap<>();
}
newAuthData.put(authType, authData);
serverData.put(KEY_AUTH_DATA, newAuthData);
return this;
}
public Builder isNew(boolean isNew) {
this.isNew = isNew;
return this;
}
}
private final boolean isNew;
private State(Builder builder) {
super(builder);
isNew = builder.isNew;
}
/* package */ State(Parcel source, String className, ParseParcelDecoder decoder) {
super(source, className, decoder);
isNew = source.readByte() == 1;
}
@SuppressWarnings("unchecked")
@Override
public Builder newBuilder() {
return new Builder(this);
}
public String sessionToken() {
return (String) get(KEY_SESSION_TOKEN);
}
@SuppressWarnings("unchecked")
public Map<String, Map<String, String>> authData() {
Map<String, Map<String, String>> authData =
(Map<String, Map<String, String>>) get(KEY_AUTH_DATA);
if (authData == null) {
// We'll always return non-null for now since we don't have any null checking in place.
// Be aware not to get and set without checking size or else we'll be adding a value that
// wasn't there in the first place.
authData = new HashMap<>();
}
return authData;
}
public boolean isNew() {
return isNew;
}
@Override
protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) {
super.writeToParcel(dest, encoder);
dest.writeByte(isNew ? (byte) 1 : 0);
}
}
// Whether the object is a currentUser. If so, it will always be persisted to disk on updates.
private boolean isCurrentUser;
/**
* Constructs a new ParseUser with no data in it. A ParseUser constructed in this way will not
* have an objectId and will not persist to the database until {@link #signUp} is called.
*/
public ParseUser() {
isCurrentUser = false;
}
@Override
/* package */ boolean needsDefaultACL() {
return false;
}
@Override
boolean isKeyMutable(String key) {
return !READ_ONLY_KEYS.contains(key);
}
@Override
/* package */ State.Builder newStateBuilder(String className) {
return new State.Builder();
}
@Override
/* package */ State getState() {
return (State) super.getState();
}
/**
* @return {@code true} if this user was created with {@link #getCurrentUser()} when no current
* user previously existed and {@link #enableAutomaticUser()} is set, false if was created by any
* other means or if a previously "lazy" user was saved remotely.
*/
/* package */ boolean isLazy() {
synchronized (mutex) {
return getObjectId() == null && ParseAnonymousUtils.isLinked(this);
}
}
/**
* Whether the ParseUser has been authenticated on this device. This will be true if the ParseUser
* was obtained via a logIn or signUp method. Only an authenticated ParseUser can be saved (with
* altered attributes) and deleted.
*/
public boolean isAuthenticated() {
synchronized (mutex) {
ParseUser current = ParseUser.getCurrentUser();
return isLazy() ||
(getState().sessionToken() != null
&& current != null
&& getObjectId().equals(current.getObjectId()));
}
}
@Override
public void remove(String key) {
if (KEY_USERNAME.equals(key)) {
throw new IllegalArgumentException("Can't remove the username key.");
}
super.remove(key);
}
@Override
/* package */ JSONObject toRest(
ParseObject.State state,
List<ParseOperationSet> operationSetQueue,
ParseEncoder objectEncoder) {
// Create a sanitized copy of operationSetQueue with `password` removed if necessary
List<ParseOperationSet> cleanOperationSetQueue = operationSetQueue;
for (int i = 0; i < operationSetQueue.size(); i++) {
ParseOperationSet operations = operationSetQueue.get(i);
if (operations.containsKey(KEY_PASSWORD)) {
if (cleanOperationSetQueue == operationSetQueue) {
cleanOperationSetQueue = new LinkedList<>(operationSetQueue);
}
ParseOperationSet cleanOperations = new ParseOperationSet(operations);
cleanOperations.remove(KEY_PASSWORD);
cleanOperationSetQueue.set(i, cleanOperations);
}
}
return super.toRest(state, cleanOperationSetQueue, objectEncoder);
}
/* package for tests */ Task<Void> cleanUpAuthDataAsync() {
ParseAuthenticationManager controller = getAuthenticationManager();
Map<String, Map<String, String>> authData;
synchronized (mutex) {
authData = getState().authData();
if (authData.size() == 0) {
return Task.forResult(null); // Nothing to see or do here...
}
}
List<Task<Void>> tasks = new ArrayList<>();
Iterator<Map.Entry<String, Map<String, String>>> i = authData.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<String, Map<String, String>> entry = i.next();
if (entry.getValue() == null) {
i.remove();
tasks.add(controller.restoreAuthenticationAsync(entry.getKey(), null).makeVoid());
}
}
State newState = getState().newBuilder()
.authData(authData)
.build();
setState(newState);
return Task.whenAll(tasks);
}
@Override
/* package */ Task<Void> handleSaveResultAsync(
ParseObject.State result, ParseOperationSet operationsBeforeSave) {
boolean success = result != null;
if (success) {
operationsBeforeSave.remove(KEY_PASSWORD);
}
return super.handleSaveResultAsync(result, operationsBeforeSave);
}
@Override
/* package */ void validateSaveEventually() throws ParseException {
if (isDirty(KEY_PASSWORD)) {
// TODO(mengyan): Unify the exception we throw when validate fails
throw new ParseException(
ParseException.OTHER_CAUSE,
"Unable to saveEventually on a ParseUser with dirty password");
}
}
//region Getter/Setter helper methods
/* package */ boolean isCurrentUser() {
synchronized (mutex) {
return isCurrentUser;
}
}
/* package */ void setIsCurrentUser(boolean isCurrentUser) {
synchronized (mutex) {
this.isCurrentUser = isCurrentUser;
}
}
/**
* @return the session token for a user, if they are logged in.
*/
public String getSessionToken() {
return getState().sessionToken();
}
// This is only used when upgrading to revocable session
private Task<Void> setSessionTokenInBackground(String newSessionToken) {
synchronized (mutex) {
State state = getState();
if (newSessionToken.equals(state.sessionToken())) {
return Task.forResult(null);
}
State.Builder builder = state.newBuilder()
.sessionToken(newSessionToken);
setState(builder.build());
return saveCurrentUserAsync(this);
}
}
/* package for testes */ Map<String, Map<String, String>> getAuthData() {
synchronized (mutex) {
Map<String, Map<String, String>> authData = getMap(KEY_AUTH_DATA);
if (authData == null) {
// We'll always return non-null for now since we don't have any null checking in place.
// Be aware not to get and set without checking size or else we'll be adding a value that
// wasn't there in the first place.
authData = new HashMap<>();
}
return authData;
}
}
private Map<String, String> getAuthData(String authType) {
return getAuthData().get(authType);
}
/* package */ void putAuthData(String authType, Map<String, String> authData) {
synchronized (mutex) {
Map<String, Map<String, String>> newAuthData = getAuthData();
newAuthData.put(authType, authData);
performPut(KEY_AUTH_DATA, newAuthData);
}
}
private void removeAuthData(String authType) {
synchronized (mutex) {
Map<String, Map<String, String>> newAuthData = getAuthData();
newAuthData.remove(authType);
performPut(KEY_AUTH_DATA, newAuthData);
}
}
/**
* Sets the username. Usernames cannot be null or blank.
*
* @param username
* The username to set.
*/
public void setUsername(String username) {
put(KEY_USERNAME, username);
}
/**
* Retrieves the username.
*/
public String getUsername() {
return getString(KEY_USERNAME);
}
/**
* Sets the password.
*
* @param password
* The password to set.
*/
public void setPassword(String password) {
put(KEY_PASSWORD, password);
}
/* package for tests */ String getPassword() {
return getString(KEY_PASSWORD);
}
/**
* Sets the email address.
*
* @param email
* The email address to set.
*/
public void setEmail(String email) {
put(KEY_EMAIL, email);
}
/**
* Retrieves the email address.
*/
public String getEmail() {
return getString(KEY_EMAIL);
}
/**
* Indicates whether this {@code ParseUser} was created during this session through a call to
* {@link #signUp()} or by logging in with a linked service such as Facebook.
*/
public boolean isNew() {
return getState().isNew();
}
//endregion
@Override
public void put(String key, Object value) {
synchronized (mutex) {
if (KEY_USERNAME.equals(key)) {
// When the username is set, remove any semblance of anonymity.
stripAnonymity();
}
super.put(key, value);
}
}
private void stripAnonymity() {
synchronized (mutex) {
if (ParseAnonymousUtils.isLinked(this)) {
if (getObjectId() != null) {
putAuthData(ParseAnonymousUtils.AUTH_TYPE, null);
} else {
removeAuthData(ParseAnonymousUtils.AUTH_TYPE);
}
}
}
}
// TODO(grantland): Can we replace this with #revert(String)?
private void restoreAnonymity(Map<String, String> anonymousData) {
synchronized (mutex) {
if (anonymousData != null) {
putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousData);
}
}
}
@Override
/* package */ void validateSave() {
synchronized (mutex) {
if (getObjectId() == null) {
throw new IllegalArgumentException(
"Cannot save a ParseUser until it has been signed up. Call signUp first.");
}
if (isAuthenticated() || !isDirty() || isCurrentUser()) {
return;
}
}
if (!Parse.isLocalDatastoreEnabled()) {
// This might be a different of instance of the currentUser, so we need to check objectIds
ParseUser current = ParseUser.getCurrentUser(); //TODO (grantland): possible blocking disk i/o
if (current != null && getObjectId().equals(current.getObjectId())) {
return;
}
}
throw new IllegalArgumentException("Cannot save a ParseUser that is not authenticated.");
}
@Override
/* package */ Task<Void> saveAsync(String sessionToken, Task<Void> toAwait) {
return saveAsync(sessionToken, isLazy(), toAwait);
}
/* package for tests */ Task<Void> saveAsync(String sessionToken, boolean isLazy, Task<Void> toAwait) {
Task<Void> task;
if (isLazy) {
task = resolveLazinessAsync(toAwait);
} else {
task = super.saveAsync(sessionToken, toAwait);
}
if (isCurrentUser()) {
// If the user is the currently logged in user, we persist all data to disk
return task.onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return cleanUpAuthDataAsync();
}
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return saveCurrentUserAsync(ParseUser.this);
}
});
}
return task;
}
@Override
/* package */ void setState(ParseObject.State newState) {
if (isCurrentUser()) {
State.Builder newStateBuilder = newState.newBuilder();
// Avoid clearing sessionToken when updating the current user's State via ParseQuery result
if (getSessionToken() != null && newState.get(KEY_SESSION_TOKEN) == null) {
newStateBuilder.put(KEY_SESSION_TOKEN, getSessionToken());
}
// Avoid clearing authData when updating the current user's State via ParseQuery result
if (getAuthData().size() > 0 && newState.get(KEY_AUTH_DATA) == null) {
newStateBuilder.put(KEY_AUTH_DATA, getAuthData());
}
newState = newStateBuilder.build();
}
super.setState(newState);
}
@Override
/* package */ void validateDelete() {
synchronized (mutex) {
super.validateDelete();
if (!isAuthenticated() && isDirty()) {
throw new IllegalArgumentException("Cannot delete a ParseUser that is not authenticated.");
}
}
}
@SuppressWarnings("unchecked")
@Override
public ParseUser fetch() throws ParseException {
return (ParseUser) super.fetch();
}
@SuppressWarnings("unchecked")
@Override
/* package */ <T extends ParseObject> Task<T> fetchAsync(
String sessionToken, Task<Void> toAwait) {
//TODO (grantland): It doesn't seem like we should do this.. Why don't we error like we do
// when fetching an unsaved ParseObject?
if (isLazy()) {
return Task.forResult((T) this);
}
Task<T> task = super.fetchAsync(sessionToken, toAwait);
if (isCurrentUser()) {
return task.onSuccessTask(new Continuation<T, Task<Void>>() {
@Override
public Task<Void> then(final Task<T> fetchAsyncTask) throws Exception {
return cleanUpAuthDataAsync();
}
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return saveCurrentUserAsync(ParseUser.this);
}
}).onSuccess(new Continuation<Void, T>() {
@Override
public T then(Task<Void> task) throws Exception {
return (T) ParseUser.this;
}
});
}
return task;
}
/**
* Signs up a new user. You should call this instead of {@link #save} for new ParseUsers. This
* will create a new ParseUser on the server, and also persist the session on disk so that you can
* access the user using {@link #getCurrentUser}.
* <p/>
* A username and password must be set before calling signUp.
* <p/>
* This is preferable to using {@link #signUp}, unless your code is already running from a
* background thread.
*
* @return A Task that is resolved when sign up completes.
*/
public Task<Void> signUpInBackground() {
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return signUpAsync(task);
}
});
}
/* package for tests */ Task<Void> signUpAsync(Task<Void> toAwait) {
final ParseUser user = getCurrentUser(); //TODO (grantland): convert to async
synchronized (mutex) {
final String sessionToken = user != null ? user.getSessionToken() : null;
if (ParseTextUtils.isEmpty(getUsername())) {
return Task.forError(new IllegalArgumentException("Username cannot be missing or blank"));
}
if (ParseTextUtils.isEmpty(getPassword())) {
return Task.forError(new IllegalArgumentException("Password cannot be missing or blank"));
}
if (getObjectId() != null) {
// For anonymous users, there may be an objectId. Setting the
// userName will have removed the anonymous link and set the
// value in the authData object to JSONObject.NULL, so we can
// just treat it like a save operation.
Map<String, Map<String, String>> authData = getAuthData();
if (authData.containsKey(ParseAnonymousUtils.AUTH_TYPE)
&& authData.get(ParseAnonymousUtils.AUTH_TYPE) == null) {
return saveAsync(sessionToken, toAwait);
}
// Otherwise, throw.
return Task.forError(
new IllegalArgumentException("Cannot sign up a user that has already signed up."));
}
// If the operationSetQueue is has operation sets in it, then a save or signUp is in progress.
// If there is a signUp or save already in progress, don't allow another one to start.
if (operationSetQueue.size() > 1) {
return Task.forError(
new IllegalArgumentException("Cannot sign up a user that is already signing up."));
}
// If the current user is an anonymous user, merge this object's data into the anonymous user
// and save.
if (user != null && ParseAnonymousUtils.isLinked(user)) {
// this doesn't have any outstanding saves, so we can safely merge its operations into the
// current user.
if (this == user) {
return Task.forError(
new IllegalArgumentException("Attempt to merge currentUser with itself."));
}
boolean isLazy = user.isLazy();
final String oldUsername = user.getUsername();
final String oldPassword = user.getPassword();
final Map<String, String> anonymousData = user.getAuthData(ParseAnonymousUtils.AUTH_TYPE);
user.copyChangesFrom(this);
user.setUsername(getUsername());
user.setPassword(getPassword());
revert();
return user.saveAsync(sessionToken, isLazy, toAwait).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
if (task.isCancelled() || task.isFaulted()) { // Error
synchronized (user.mutex) {
if (oldUsername != null) {
user.setUsername(oldUsername);
} else {
user.revert(KEY_USERNAME);
}
if (oldPassword != null) {
user.setPassword(oldPassword);
} else {
user.revert(KEY_PASSWORD);
}
user.restoreAnonymity(anonymousData);
}
return task;
} else { // Success
user.revert(KEY_PASSWORD);
revert(KEY_PASSWORD);
}
mergeFromObject(user);
return saveCurrentUserAsync(ParseUser.this);
}
});
}
final ParseOperationSet operations = startSave();
return toAwait.onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return getUserController().signUpAsync(
getState(), operations, sessionToken
).continueWithTask(new Continuation<ParseUser.State, Task<Void>>() {
@Override
public Task<Void> then(final Task<ParseUser.State> signUpTask) throws Exception {
ParseUser.State result = signUpTask.getResult();
return handleSaveResultAsync(result,
operations).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
if (!signUpTask.isCancelled() && !signUpTask.isFaulted()) {
return saveCurrentUserAsync(ParseUser.this);
}
return signUpTask.makeVoid();
}
});
}
});
}
});
}
}
/**
* Signs up a new user. You should call this instead of {@link #save} for new ParseUsers. This
* will create a new ParseUser on the server, and also persist the session on disk so that you can
* access the user using {@link #getCurrentUser}.
* <p/>
* A username and password must be set before calling signUp.
* <p/>
* Typically, you should use {@link #signUpInBackground} instead of this, unless you are managing
* your own threading.
*
* @throws ParseException
* Throws an exception if the server is inaccessible, or if the username has already
* been taken.
*/
public void signUp() throws ParseException {
ParseTaskUtils.wait(signUpInBackground());
}
/**
* Signs up a new user. You should call this instead of {@link #save} for new ParseUsers. This
* will create a new ParseUser on the server, and also persist the session on disk so that you can
* access the user using {@link #getCurrentUser}.
* <p/>
* A username and password must be set before calling signUp.
* <p/>
* This is preferable to using {@link #signUp}, unless your code is already running from a
* background thread.
*
* @param callback
* callback.done(user, e) is called when the signUp completes.
*/
public void signUpInBackground(SignUpCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(signUpInBackground(), callback);
}
/**
* Logs in a user with a username and password. On success, this saves the session to disk, so you
* can retrieve the currently logged in user using {@link #getCurrentUser}.
* <p/>
* This is preferable to using {@link #logIn}, unless your code is already running from a
* background thread.
*
* @param username
* The username to log in with.
* @param password
* The password to log in with.
*
* @return A Task that is resolved when logging in completes.
*/
public static Task<ParseUser> logInInBackground(String username, String password) {
if (username == null) {
throw new IllegalArgumentException("Must specify a username for the user to log in with");
}
if (password == null) {
throw new IllegalArgumentException("Must specify a password for the user to log in with");
}
return getUserController().logInAsync(username, password).onSuccessTask(new Continuation<State, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<State> task) throws Exception {
State result = task.getResult();
final ParseUser newCurrent = ParseObject.from(result);
return saveCurrentUserAsync(newCurrent).onSuccess(new Continuation<Void, ParseUser>() {
@Override
public ParseUser then(Task<Void> task) throws Exception {
return newCurrent;
}
});
}
});
}
/**
* Logs in a user with a username and password. On success, this saves the session to disk, so you
* can retrieve the currently logged in user using {@link #getCurrentUser}.
* <p/>
* Typically, you should use {@link #logInInBackground} instead of this, unless you are managing
* your own threading.
*
* @param username
* The username to log in with.
* @param password
* The password to log in with.
* @throws ParseException
* Throws an exception if the login was unsuccessful.
* @return The user if the login was successful.
*/
public static ParseUser logIn(String username, String password) throws ParseException {
return ParseTaskUtils.wait(logInInBackground(username, password));
}
/**
* Logs in a user with a username and password. On success, this saves the session to disk, so you
* can retrieve the currently logged in user using {@link #getCurrentUser}.
* <p/>
* This is preferable to using {@link #logIn}, unless your code is already running from a
* background thread.
*
* @param username
* The username to log in with.
* @param password
* The password to log in with.
* @param callback
* callback.done(user, e) is called when the login completes.
*/
public static void logInInBackground(final String username, final String password,
LogInCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(logInInBackground(username, password), callback);
}
/**
* Authorize a user with a session token. On success, this saves the session to disk, so you can
* retrieve the currently logged in user using {@link #getCurrentUser}.
* <p/>
* This is preferable to using {@link #become}, unless your code is already running from a
* background thread.
*
* @param sessionToken
* The session token to authorize with.
*
* @return A Task that is resolved when authorization completes.
*/
public static Task<ParseUser> becomeInBackground(String sessionToken) {
if (sessionToken == null) {
throw new IllegalArgumentException("Must specify a sessionToken for the user to log in with");
}
return getUserController().getUserAsync(sessionToken).onSuccessTask(new Continuation<State, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<State> task) throws Exception {
State result = task.getResult();
final ParseUser user = ParseObject.from(result);
return saveCurrentUserAsync(user).onSuccess(new Continuation<Void, ParseUser>() {
@Override
public ParseUser then(Task<Void> task) throws Exception {
return user;
}
});
}
});
}
/**
* Authorize a user with a session token. On success, this saves the session to disk, so you can
* retrieve the currently logged in user using {@link #getCurrentUser}.
* <p/>
* Typically, you should use {@link #becomeInBackground} instead of this, unless you are managing
* your own threading.
*
* @param sessionToken
* The session token to authorize with.
* @throws ParseException
* Throws an exception if the authorization was unsuccessful.
* @return The user if the authorization was successful.
*/
public static ParseUser become(String sessionToken) throws ParseException {
return ParseTaskUtils.wait(becomeInBackground(sessionToken));
}
/**
* Authorize a user with a session token. On success, this saves the session to disk, so you can
* retrieve the currently logged in user using {@link #getCurrentUser}.
* <p/>
* This is preferable to using {@link #become}, unless your code is already running from a
* background thread.
*
* @param sessionToken
* The session token to authorize with.
* @param callback
* callback.done(user, e) is called when the authorization completes.
*/
public static void becomeInBackground(final String sessionToken, LogInCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(becomeInBackground(sessionToken), callback);
}
//TODO (grantland): Publicize
/* package */ static Task<ParseUser> getCurrentUserAsync() {
return getCurrentUserController().getAsync();
}
/**
* This retrieves the currently logged in ParseUser with a valid session, either from memory or
* disk if necessary.
*
* @return The currently logged in ParseUser
*/
public static ParseUser getCurrentUser() {
return getCurrentUser(isAutomaticUserEnabled());
}
/**
* This retrieves the currently logged in ParseUser with a valid session, either from memory or
* disk if necessary.
*
* @param shouldAutoCreateUser
* {@code true} to automatically create and set an anonymous user as current.
* @return The currently logged in ParseUser
*/
private static ParseUser getCurrentUser(boolean shouldAutoCreateUser) {
try {
return ParseTaskUtils.wait(getCurrentUserController().getAsync(shouldAutoCreateUser));
} catch (ParseException e) {
//TODO (grantland): Publicize this exception
return null;
}
}
//TODO (grantland): Make it throw ParseException and call #getCurrenSessionTokenInBackground()
/* package */ static String getCurrentSessionToken() {
ParseUser current = ParseUser.getCurrentUser();
return current != null ? current.getSessionToken() : null;
}
//TODO (grantland): Make it really async and publicize in v2
/* package */ static Task<String> getCurrentSessionTokenAsync() {
return getCurrentUserController().getCurrentSessionTokenAsync();
}
// Persists a user as currentUser to disk, and into the singleton
private static Task<Void> saveCurrentUserAsync(ParseUser user) {
return getCurrentUserController().setAsync(user);
}
/**
* Used by {@link ParseObject#pin} to persist lazy users to LDS that haven't been pinned yet.
*/
/* package */ static Task<Void> pinCurrentUserIfNeededAsync(ParseUser user) {
if (!Parse.isLocalDatastoreEnabled()) {
throw new IllegalStateException("Method requires Local Datastore. " +
"Please refer to `Parse#enableLocalDatastore(Context)`.");
}
return getCurrentUserController().setIfNeededAsync(user);
}
/**
* Logs out the currently logged in user session. This will remove the session from disk, log out
* of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}.
* <p/>
* This is preferable to using {@link #logOut}, unless your code is already running from a
* background thread.
*
* @return A Task that is resolved when logging out completes.
*/
public static Task<Void> logOutInBackground() {
return getCurrentUserController().logOutAsync();
}
/**
* Logs out the currently logged in user session. This will remove the session from disk, log out
* of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}.
* <p/>
* This is preferable to using {@link #logOut}, unless your code is already running from a
* background thread.
*/
public static void logOutInBackground(LogOutCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(logOutInBackground(), callback);
}
/**
* Logs out the currently logged in user session. This will remove the session from disk, log out
* of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}.
* <p/>
* Typically, you should use {@link #logOutInBackground()} instead of this, unless you are
* managing your own threading.
* <p/>
* <strong>Note:</strong>: Any errors in the log out flow will be swallowed due to
* backward-compatibility reasons. Please use {@link #logOutInBackground()} if you'd wish to
* handle them.
*/
public static void logOut() {
try {
ParseTaskUtils.wait(logOutInBackground());
} catch (ParseException e) {
//TODO (grantland): We shouldn't swallow errors, but we need to for backwards compatibility.
// Change this in v2.
}
}
//TODO (grantland): Add to taskQueue
/* package */ Task<Void> logOutAsync() {
return logOutAsync(true);
}
/* package */ Task<Void> logOutAsync(boolean revoke) {
ParseAuthenticationManager controller = getAuthenticationManager();
List<Task<Void>> tasks = new ArrayList<>();
final String oldSessionToken;
synchronized (mutex) {
oldSessionToken = getState().sessionToken();
for (Map.Entry<String, Map<String, String>> entry : getAuthData().entrySet()) {
tasks.add(controller.deauthenticateAsync(entry.getKey()));
}
State newState = getState().newBuilder()
.sessionToken(null)
.isNew(false)
.build();
isCurrentUser = false;
setState(newState);
}
if (revoke) {
tasks.add(ParseSession.revokeAsync(oldSessionToken));
}
return Task.whenAll(tasks);
}
/**
* Requests a password reset email to be sent in a background thread to the specified email
* address associated with the user account. This email allows the user to securely reset their
* password on the Parse site.
* <p/>
* This is preferable to using {@link #requestPasswordReset(String)}, unless your code is already
* running from a background thread.
*
* @param email
* The email address associated with the user that forgot their password.
*
* @return A Task that is resolved when the command completes.
*/
public static Task<Void> requestPasswordResetInBackground(String email) {
return getUserController().requestPasswordResetAsync(email);
}
/**
* Requests a password reset email to be sent to the specified email address associated with the
* user account. This email allows the user to securely reset their password on the Parse site.
* <p/>
* Typically, you should use {@link #requestPasswordResetInBackground} instead of this, unless you
* are managing your own threading.
*
* @param email
* The email address associated with the user that forgot their password.
* @throws ParseException
* Throws an exception if the server is inaccessible, or if an account with that email
* doesn't exist.
*/
public static void requestPasswordReset(String email) throws ParseException {
ParseTaskUtils.wait(requestPasswordResetInBackground(email));
}
/**
* Requests a password reset email to be sent in a background thread to the specified email
* address associated with the user account. This email allows the user to securely reset their
* password on the Parse site.
* <p/>
* This is preferable to using {@link #requestPasswordReset(String)}, unless your code is already
* running from a background thread.
*
* @param email
* The email address associated with the user that forgot their password.
* @param callback
* callback.done(e) is called when the request completes.
*/
public static void requestPasswordResetInBackground(final String email,
RequestPasswordResetCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(requestPasswordResetInBackground(email), callback);
}
@SuppressWarnings("unchecked")
@Override
public ParseUser fetchIfNeeded() throws ParseException {
return super.fetchIfNeeded();
}
//region Third party authentication
/**
* Registers a third party authentication callback.
* <p />
* <strong>Note:</strong> This shouldn't be called directly unless developing a third party authentication
* library.
*
* @param authType The name of the third party authentication source.
* @param callback The third party authentication callback to be registered.
*
* @see AuthenticationCallback
*/
public static void registerAuthenticationCallback(
String authType, AuthenticationCallback callback) {
getAuthenticationManager().register(authType, callback);
}
/**
* Logs in a user with third party authentication credentials.
* <p />
* <strong>Note:</strong> This shouldn't be called directly unless developing a third party authentication
* library.
*
* @param authType The name of the third party authentication source.
* @param authData The user credentials of the third party authentication source.
* @return A {@code Task} is resolved when logging in completes.
*
* @see AuthenticationCallback
*/
public static Task<ParseUser> logInWithInBackground(
final String authType, final Map<String, String> authData) {
if (authType == null) {
throw new IllegalArgumentException("Invalid authType: " + null);
}
final Continuation<Void, Task<ParseUser>> logInWithTask = new Continuation<Void, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<Void> task) throws Exception {
return getUserController().logInAsync(authType, authData).onSuccessTask(new Continuation<ParseUser.State, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<ParseUser.State> task) throws Exception {
ParseUser.State result = task.getResult();
final ParseUser user = ParseObject.from(result);
return saveCurrentUserAsync(user).onSuccess(new Continuation<Void, ParseUser>() {
@Override
public ParseUser then(Task<Void> task) throws Exception {
return user;
}
});
}
});
}
};
// Handle claiming of user.
return getCurrentUserController().getAsync(false).onSuccessTask(new Continuation<ParseUser, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<ParseUser> task) throws Exception {
final ParseUser user = task.getResult();
if (user != null) {
synchronized (user.mutex) {
if (ParseAnonymousUtils.isLinked(user)) {
if (user.isLazy()) {
final Map<String, String> oldAnonymousData =
user.getAuthData(ParseAnonymousUtils.AUTH_TYPE);
return user.taskQueue.enqueue(new Continuation<Void, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(final Task<Void> toAwait) throws Exception {
return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
synchronized (user.mutex) {
// Replace any anonymity with the new linked authData.
user.stripAnonymity();
user.putAuthData(authType, authData);
return user.resolveLazinessAsync(task);
}
}
}).continueWithTask(new Continuation<Void, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<Void> task) throws Exception {
synchronized (user.mutex) {
if (task.isFaulted()) {
user.removeAuthData(authType);
user.restoreAnonymity(oldAnonymousData);
return Task.forError(task.getError());
}
if (task.isCancelled()) {
return Task.cancelled();
}
return Task.forResult(user);
}
}
});
}
});
} else {
// Try to link the current user with third party user, unless a user is already linked
// to that third party user, then we'll just create a new user and link it with the
// third party user. New users will not be linked to the previous user's data.
return user.linkWithInBackground(authType, authData)
.continueWithTask(new Continuation<Void, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<Void> task) throws Exception {
if (task.isFaulted()) {
Exception error = task.getError();
if (error instanceof ParseException
&& ((ParseException) error).getCode() == ParseException.ACCOUNT_ALREADY_LINKED) {
// An account that's linked to the given authData already exists, so log in
// instead of trying to claim.
return Task.<Void>forResult(null).continueWithTask(logInWithTask);
}
}
if (task.isCancelled()) {
return Task.cancelled();
}
return Task.forResult(user);
}
});
}
}
}
}
return Task.<Void>forResult(null).continueWithTask(logInWithTask);
}
});
}
/**
* Indicates whether this user is linked with a third party authentication source.
* <p />
* <strong>Note:</strong> This shouldn't be called directly unless developing a third party authentication
* library.
*
* @param authType The name of the third party authentication source.
* @return {@code true} if linked, otherwise {@code false}.
*
* @see AuthenticationCallback
*/
public boolean isLinked(String authType) {
Map<String, Map<String, String>> authData = getAuthData();
return authData.containsKey(authType) && authData.get(authType) != null;
}
/**
* Ensures that all auth sources have auth data (e.g. access tokens, etc.) that matches this
* user.
*/
/* package */ Task<Void> synchronizeAllAuthDataAsync() {
Map<String, Map<String, String>> authData;
synchronized (mutex) {
if (!isCurrentUser()) {
return Task.forResult(null);
}
authData = getAuthData();
}
List<Task<Void>> tasks = new ArrayList<>(authData.size());
for (String authType : authData.keySet()) {
tasks.add(synchronizeAuthDataAsync(authType));
}
return Task.whenAll(tasks);
}
/* package */ Task<Void> synchronizeAuthDataAsync(String authType) {
Map<String, String> authData;
synchronized (mutex) {
if (!isCurrentUser()) {
return Task.forResult(null);
}
authData = getAuthData(authType);
}
return synchronizeAuthDataAsync(getAuthenticationManager(), authType, authData);
}
private Task<Void> synchronizeAuthDataAsync(
ParseAuthenticationManager manager, final String authType, Map<String, String> authData) {
return manager.restoreAuthenticationAsync(authType, authData).continueWithTask(new Continuation<Boolean, Task<Void>>() {
@Override
public Task<Void> then(Task<Boolean> task) throws Exception {
boolean success = !task.isFaulted() && task.getResult();
if (!success) {
return unlinkFromInBackground(authType);
}
return task.makeVoid();
}
});
}
private Task<Void> linkWithAsync(
final String authType,
final Map<String, String> authData,
final Task<Void> toAwait,
final String sessionToken) {
synchronized (mutex) {
final boolean isLazy = isLazy();
final Map<String, String> oldAnonymousData = getAuthData(ParseAnonymousUtils.AUTH_TYPE);
stripAnonymity();
putAuthData(authType, authData);
return saveAsync(sessionToken, isLazy, toAwait).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
synchronized (mutex) {
if (task.isFaulted() || task.isCancelled()) {
removeAuthData(authType);
restoreAnonymity(oldAnonymousData);
return task;
}
return synchronizeAuthDataAsync(authType);
}
}
});
}
}
private Task<Void> linkWithAsync(
final String authType,
final Map<String, String> authData,
final String sessionToken) {
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return linkWithAsync(authType, authData, task, sessionToken);
}
});
}
/**
* Links this user to a third party authentication source.
* <p />
* <strong>Note:</strong> This shouldn't be called directly unless developing a third party authentication
* library.
*
* @param authType The name of the third party authentication source.
* @param authData The user credentials of the third party authentication source.
* @return A {@code Task} is resolved when linking completes.
*
* @see AuthenticationCallback
*/
public Task<Void> linkWithInBackground(
String authType, Map<String, String> authData) {
if (authType == null) {
throw new IllegalArgumentException("Invalid authType: " + null);
}
return linkWithAsync(authType, authData, getSessionToken());
}
/**
* Unlinks this user from a third party authentication source.
* <p />
* <strong>Note:</strong> This shouldn't be called directly unless developing a third party authentication
* library.
*
* @param authType The name of the third party authentication source.
* @return A {@code Task} is resolved when unlinking completes.
*
* @see AuthenticationCallback
*/
public Task<Void> unlinkFromInBackground(final String authType) {
if (authType == null) {
return Task.forResult(null);
}
synchronized (mutex) {
if (!getAuthData().containsKey(authType)) {
return Task.forResult(null);
}
putAuthData(authType, null);
}
return saveInBackground();
}
//endregion
/**
* Try to resolve a lazy user.
*
* If {@code authData} is empty, we'll treat this just as a SignUp. Otherwise, we'll
* treat this as a SignUpOrLogIn. We'll merge the server result with this user, only if LDS is not
* enabled.
*
* @param toAwait {@code Task} to wait for completion before running.
* @return A {@code Task} that will resolve to the current user. If this is a SignUp it'll be this
* {@code ParseUser} instance, otherwise it'll be a new {@code ParseUser} instance.
*/
/* package for tests */ Task<Void> resolveLazinessAsync(Task<Void> toAwait) {
synchronized (mutex) {
if (getAuthData().size() == 0) { // TODO(grantland): Could we just check isDirty(KEY_AUTH_DATA)?
// If there are no linked services, treat this as a SignUp.
return signUpAsync(toAwait);
}
final ParseOperationSet operations = startSave();
// Otherwise, treat this as a SignUpOrLogIn
return toAwait.onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return getUserController().logInAsync(getState(), operations).onSuccessTask(new Continuation<ParseUser.State, Task<Void>>() {
@Override
public Task<Void> then(Task<ParseUser.State> task) throws Exception {
final ParseUser.State result = task.getResult();
Task<ParseUser.State> resultTask;
// We can't merge this user with the server if this is a LogIn because LDS might
// already be keeping track of the servers objectId.
if (Parse.isLocalDatastoreEnabled() && !result.isNew()) {
resultTask = Task.forResult(result);
} else {
resultTask = handleSaveResultAsync(result,
operations).onSuccess(new Continuation<Void, ParseUser.State>() {
@Override
public ParseUser.State then(Task<Void> task) throws Exception {
return result;
}
});
}
return resultTask.onSuccessTask(new Continuation<ParseUser.State, Task<Void>>() {
@Override
public Task<Void> then(Task<ParseUser.State> task) throws Exception {
ParseUser.State result = task.getResult();
if (!result.isNew()) {
// If the result is not a new user, treat this as a fresh logIn with complete
// serverData, and switch the current user to the new user.
final ParseUser newUser = ParseObject.from(result);
return saveCurrentUserAsync(newUser);
}
return task.makeVoid();
}
});
}
});
}
});
}
}
@SuppressWarnings("unchecked")
@Override
/* package */ <T extends ParseObject> Task<T> fetchFromLocalDatastoreAsync() {
// Same as #fetch()
if (isLazy()) {
return Task.forResult((T) this);
}
return super.fetchFromLocalDatastoreAsync();
}
//region Automatic User
private static final Object isAutoUserEnabledMutex = new Object();
private static boolean autoUserEnabled;
/**
* Enables automatic creation of anonymous users. After calling this method,
* {@link #getCurrentUser()} will always have a value. The user will only be created on the server
* once the user has been saved, or once an object with a relation to that user or an ACL that
* refers to the user has been saved.
* <p/>
* <strong>Note:</strong> {@link ParseObject#saveEventually()} will not work if an item being
* saved has a relation to an automatic user that has never been saved.
*/
public static void enableAutomaticUser() {
synchronized (isAutoUserEnabledMutex) {
autoUserEnabled = true;
}
}
/* package */ static void disableAutomaticUser() {
synchronized (isAutoUserEnabledMutex) {
autoUserEnabled = false;
}
}
/* package */ static boolean isAutomaticUserEnabled() {
synchronized (isAutoUserEnabledMutex) {
return autoUserEnabled;
}
}
//endregion
//region Parcelable
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
synchronized (mutex) {
outState.putBoolean(PARCEL_KEY_IS_CURRENT_USER, isCurrentUser);
}
}
@Override
protected void onRestoreInstanceState(Bundle savedState) {
super.onRestoreInstanceState(savedState);
setIsCurrentUser(savedState.getBoolean(PARCEL_KEY_IS_CURRENT_USER, false));
}
//endregion
//region Legacy/Revocable Session Tokens
/**
* Enables revocable sessions. This method is only required if you wish to use
* {@link ParseSession} APIs and do not have revocable sessions enabled in your application
* settings on your parse server.
* <p/>
* Upon successful completion of this {@link Task}, {@link ParseSession} APIs will be available
* for use.
*
* @return A {@link Task} that will resolve when enabling revocable session
*/
public static Task<Void> enableRevocableSessionInBackground() {
// TODO(mengyan): Right now there is no way for us to add interceptor for this client,
// so maybe we should move add interceptor steps to restClient()
ParseCorePlugins.getInstance().registerUserController(
new NetworkUserController(ParsePlugins.get().restClient(), true));
return getCurrentUserController().getAsync(false).onSuccessTask(new Continuation<ParseUser, Task<Void>>() {
@Override
public Task<Void> then(Task<ParseUser> task) throws Exception {
ParseUser user = task.getResult();
if (user == null) {
return Task.forResult(null);
}
return user.upgradeToRevocableSessionAsync();
}
});
}
/* package */ Task<Void> upgradeToRevocableSessionAsync() {
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> toAwait) throws Exception {
return upgradeToRevocableSessionAsync(toAwait);
}
});
}
private Task<Void> upgradeToRevocableSessionAsync(Task<Void> toAwait) {
final String sessionToken = getSessionToken();
return toAwait.continueWithTask(new Continuation<Void, Task<String>>() {
@Override
public Task<String> then(Task<Void> task) throws Exception {
return ParseSession.upgradeToRevocableSessionAsync(sessionToken);
}
}).onSuccessTask(new Continuation<String, Task<Void>>() {
@Override
public Task<Void> then(Task<String> task) throws Exception {
String result = task.getResult();
return setSessionTokenInBackground(result);
}
});
}
//endregion
}