// Copyright 2015 The Project Buendia Authors // // 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 distrib- // uted 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 // specific language governing permissions and limitations under the License. package org.projectbuendia.client.user; import android.content.OperationApplicationException; import android.os.AsyncTask; import android.os.RemoteException; import com.android.volley.VolleyError; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import org.projectbuendia.client.events.user.ActiveUserSetEvent; import org.projectbuendia.client.events.user.ActiveUserUnsetEvent; import org.projectbuendia.client.events.user.KnownUsersLoadFailedEvent; import org.projectbuendia.client.events.user.KnownUsersLoadedEvent; import org.projectbuendia.client.events.user.KnownUsersSyncFailedEvent; import org.projectbuendia.client.events.user.KnownUsersSyncedEvent; import org.projectbuendia.client.events.user.UserAddFailedEvent; import org.projectbuendia.client.events.user.UserAddedEvent; import org.projectbuendia.client.json.JsonNewUser; import org.projectbuendia.client.json.JsonUser; import org.projectbuendia.client.utils.AsyncTaskRunner; import org.projectbuendia.client.utils.EventBusInterface; import org.projectbuendia.client.utils.Logger; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; /** * Manages the available logins and the currently logged-in user. * <p/> * <p>All classes that care about the current active user should be able to gracefully handle the * following event bus events: * <ul> * <li>{@link ActiveUserSetEvent} * <li>{@link ActiveUserUnsetEvent} * </ul> * <p/> * <p>All classes that care about all known users should additionally be able to gracefully handle * the following event bus events: * <ul> * <li>{@link KnownUsersLoadedEvent} * <li>{@link KnownUsersLoadFailedEvent} * <li>{@link KnownUsersSyncedEvent} * <li>{@link KnownUsersSyncFailedEvent} * </ul> * <p/> * <p>All classes that care about being able to add and delete users should additionally be able * gracefully handle the following event bus events: * <ul> * <li>{@link UserAddedEvent} * <li>{@link UserAddFailedEvent} * </ul> * <p/> * <p>All methods should be called on the main thread. */ public class UserManager { private static final Logger LOG = Logger.create(); private final UserStore mUserStore; private final EventBusInterface mEventBus; private final AsyncTaskRunner mAsyncTaskRunner; private final Set<JsonUser> mKnownUsers = new HashSet<>(); private boolean mSynced = false; private boolean mAutoCancelEnabled = false; private boolean mIsDirty = false; @Nullable private AsyncTask mLastTask; @Nullable private JsonUser mActiveUser; /** * Utility function for automatically canceling user load tasks to simulate network connectivity * issues. * TODO: Move to a fake or mock out when daggered. */ public void setAutoCancelEnabled(boolean autoCancelEnabled) { mAutoCancelEnabled = autoCancelEnabled; } /** Resets the UserManager to its initial empty state. */ public void reset() { mKnownUsers.clear(); mSynced = false; } /** * If true, users have been recently updated and any data relying on a specific view of users * may be out of sync. */ public boolean isDirty() { return mIsDirty; } /** Sets whether or not users have been recently updated. */ public void setDirty(boolean shouldInvalidateFormCache) { mIsDirty = shouldInvalidateFormCache; } /** * Loads the set of all users known to the application from local cache. * <p/> * <p>This method will post a {@link KnownUsersLoadedEvent} if the known users were * successfully loaded and a {@link KnownUsersLoadFailedEvent} otherwise. * <p/> * <p>This method will only perform a local cache lookup once per application lifetime. */ public void loadKnownUsers() { if (!mSynced) { mLastTask = new LoadKnownUsersTask(); mAsyncTaskRunner.runTask(mLastTask); } else { mEventBus.post(new KnownUsersLoadedEvent(ImmutableSet.copyOf(mKnownUsers))); } } /** Sync users synchronously. Blocks until the list of users is synced, or interrupted. */ public void syncKnownUsersSynchronously() throws InterruptedException, ExecutionException, RemoteException, OperationApplicationException, UserSyncException { onUsersSynced(mUserStore.syncKnownUsers()); } /** * Called when users are retrieved from the server, in order to send events and update user * state as necessary. */ private void onUsersSynced(Set<JsonUser> syncedUsers) throws UserSyncException { if (syncedUsers == null || syncedUsers.isEmpty()) { throw new UserSyncException("Set of users retrieved from server is null or empty."); } ImmutableSet<JsonUser> addedUsers = ImmutableSet.copyOf(Sets.difference(syncedUsers, mKnownUsers)); ImmutableSet<JsonUser> deletedUsers = ImmutableSet.copyOf(Sets.difference(mKnownUsers, syncedUsers)); mKnownUsers.clear(); mKnownUsers.addAll(syncedUsers); mEventBus.post(new KnownUsersSyncedEvent(addedUsers, deletedUsers)); if (mActiveUser != null && deletedUsers.contains(mActiveUser)) { // TODO: Potentially clear mActiveUser here. mEventBus.post(new ActiveUserUnsetEvent( mActiveUser, ActiveUserUnsetEvent.REASON_USER_DELETED)); } // If at least one user was added or deleted, the set of known users has changed. if (!addedUsers.isEmpty() || !deletedUsers.isEmpty()) { setDirty(true); } } /** Returns the current active user or {@code null} if no user is active. */ @Nullable public JsonUser getActiveUser() { return mActiveUser; } /** * Sets the current active user or unsets it if {@code activeUser} is {@code null}, returning * whether the operation succeeded. * <p/> * <p>This method will fail if the specified user is not known to the application. * <p/> * <p>This method will post an {@link ActiveUserSetEvent} if the active user was successfully * set and an {@link ActiveUserUnsetEvent} if the active user was unset successfully; these * events will be posted even if the active user did not change. */ public boolean setActiveUser(@Nullable JsonUser activeUser) { @Nullable JsonUser previousActiveUser = mActiveUser; if (activeUser == null) { mActiveUser = null; mEventBus.post(new ActiveUserUnsetEvent( previousActiveUser, ActiveUserUnsetEvent.REASON_UNSET_INVOKED)); return true; } if (!mKnownUsers.contains(activeUser)) { LOG.e("Couldn't switch user -- new user is not known"); return false; } mActiveUser = activeUser; mEventBus.post(new ActiveUserSetEvent(previousActiveUser, activeUser)); return true; } /** * Adds a user to the set of known users, both locally and on the server. * <p/> * <p>This method will post a {@link UserAddedEvent} if the user was added successfully and a * {@link UserAddFailedEvent} otherwise. */ public void addUser(JsonNewUser user) { checkNotNull(user); // TODO: Validate user. mAsyncTaskRunner.runTask(new AddUserTask(user)); } /** Thrown when an error occurs syncing users from server. */ public static class UserSyncException extends Throwable { public UserSyncException(String s) { super(s); } } UserManager( UserStore userStore, EventBusInterface eventBus, AsyncTaskRunner asyncTaskRunner) { mAsyncTaskRunner = checkNotNull(asyncTaskRunner); mEventBus = checkNotNull(eventBus); mUserStore = checkNotNull(userStore); } /** * Loads known users from the database into memory. * <p/> * <p>Forces a network sync if the database has not been downloaded yet. */ private class LoadKnownUsersTask extends AsyncTask<Object, Void, Set<JsonUser>> { @Override protected Set<JsonUser> doInBackground(Object... unusedObjects) { if (mAutoCancelEnabled) { cancel(true); return null; } try { return mUserStore.loadKnownUsers(); } catch (Exception e) { // TODO: Figure out type of exception to throw. LOG.e(e, "Load users task failed"); mEventBus.post( new KnownUsersLoadFailedEvent(KnownUsersLoadFailedEvent.REASON_UNKNOWN)); return null; } } @Override protected void onCancelled() { LOG.w("Load users task cancelled"); mEventBus.post( new KnownUsersLoadFailedEvent(KnownUsersLoadFailedEvent.REASON_CANCELLED)); } @Override protected void onPostExecute(Set<JsonUser> knownUsers) { mKnownUsers.clear(); if (knownUsers != null) { mKnownUsers.addAll(knownUsers); } mSynced = true; mEventBus.post(new KnownUsersLoadedEvent(ImmutableSet.copyOf(mKnownUsers))); } } /** Adds a user to the database asynchronously. */ private final class AddUserTask extends AsyncTask<Void, Void, JsonUser> { private final JsonNewUser mUser; private boolean mAlreadyExists; private boolean mFailedToConnect; public AddUserTask(JsonNewUser user) { mUser = checkNotNull(user); } @Override protected JsonUser doInBackground(Void... voids) { try { return mUserStore.addUser(mUser); } catch (VolleyError e) { if (e.getMessage() != null) { if (e.getMessage().contains("already in use")) { mAlreadyExists = true; } else if (e.getMessage().contains("failed to connect")) { mFailedToConnect = true; } } return null; } } @Override protected void onPostExecute(JsonUser addedUser) { if (addedUser != null) { mKnownUsers.add(addedUser); mEventBus.post(new UserAddedEvent(addedUser)); // Set of known users has changed. setDirty(true); } else if (mAlreadyExists) { mEventBus.post(new UserAddFailedEvent( mUser, UserAddFailedEvent.REASON_USER_EXISTS_ON_SERVER)); } else if (mFailedToConnect) { mEventBus.post(new UserAddFailedEvent( mUser, UserAddFailedEvent.REASON_CONNECTION_ERROR)); } else { mEventBus.post(new UserAddFailedEvent(mUser, UserAddFailedEvent.REASON_UNKNOWN)); } } } }