// 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.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.database.Cursor;
import android.os.RemoteException;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.RequestFuture;
import com.google.common.collect.ImmutableSet;
import net.sqlcipher.database.SQLiteException;
import org.projectbuendia.client.App;
import org.projectbuendia.client.json.JsonNewUser;
import org.projectbuendia.client.json.JsonUser;
import org.projectbuendia.client.providers.BuendiaProvider;
import org.projectbuendia.client.providers.Contracts;
import org.projectbuendia.client.providers.Contracts.Users;
import org.projectbuendia.client.providers.SQLiteDatabaseTransactionHelper;
import org.projectbuendia.client.utils.Logger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
/** A store for users. */
public class UserStore {
private static final Logger LOG = Logger.create();
private static final String USER_SYNC_SAVEPOINT_NAME = "USER_SYNC_SAVEPOINT";
/**
* Loads the known users from local store. If there is no user in db or the application
* can't retrieve from there, then it fetches the users from server
* */
public Set<JsonUser> loadKnownUsers()
throws InterruptedException, ExecutionException, RemoteException,
OperationApplicationException {
Set<JsonUser> users = getUsersFromDb();
if(users.isEmpty()) {
LOG.i("Database contains no user; fetching from server");
users = syncKnownUsers();
}
LOG.i(String.format("Found %d users in db", users.size()));
return users;
}
/** Syncs known users with the server. */
public Set<JsonUser> syncKnownUsers()
throws ExecutionException, InterruptedException, RemoteException,
OperationApplicationException {
Set<JsonUser> users = getUsersFromServer();
updateDatabase(users);
return users;
}
/** Adds a new user, both locally and on the server. */
public JsonUser addUser(JsonNewUser user) throws VolleyError {
JsonUser newUser = addUserOnServer(user);
addUserLocally(newUser);
return newUser;
}
private void addUserLocally(JsonUser user) {
LOG.i("Updating user db with newly added user");
ContentProviderClient client = App.getInstance().getContentResolver()
.acquireContentProviderClient(Users.CONTENT_URI);
try {
ContentValues values = new ContentValues();
values.put(Users.UUID, user.id);
values.put(Users.FULL_NAME, user.fullName);
client.insert(Users.CONTENT_URI, values);
} catch (RemoteException e) {
LOG.e(e, "Failed to update database");
} finally {
client.release();
}
}
private JsonUser addUserOnServer(JsonNewUser user) throws VolleyError {
// Define a container for the results.
class Result {
public JsonUser user = null;
public VolleyError error = null;
}
final Result result = new Result();
// Make an async call to the server and use a CountDownLatch to block until the result is
// returned.
final CountDownLatch latch = new CountDownLatch(1);
App.getServer().addUser(
user,
new Response.Listener<JsonUser>() {
@Override
public void onResponse(JsonUser response) {
result.user = response;
latch.countDown();
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
LOG.e(error, "Unexpected error adding user");
result.error = error;
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
LOG.e(e, "Interrupted while loading user list");
}
if (result.error != null) {
throw result.error;
}
return result.user;
}
private void updateDatabase(Set<JsonUser> users) throws RemoteException, OperationApplicationException {
LOG.i("Updating local database with %d users", users.size());
ContentProviderClient client = App.getInstance().getContentResolver()
.acquireContentProviderClient(Users.CONTENT_URI);
BuendiaProvider buendiaProvider =
(BuendiaProvider) (client.getLocalContentProvider());
SQLiteDatabaseTransactionHelper dbTransactionHelper =
buendiaProvider.getDbTransactionHelper();
try {
LOG.d("Setting savepoint %s", USER_SYNC_SAVEPOINT_NAME);
dbTransactionHelper.startNamedTransaction(USER_SYNC_SAVEPOINT_NAME);
client.applyBatch(getUserUpdateOps(users, new SyncResult()));
} catch (RemoteException | OperationApplicationException e) {
LOG.d("Rolling back savepoint %s", USER_SYNC_SAVEPOINT_NAME);
dbTransactionHelper.rollbackNamedTransaction(USER_SYNC_SAVEPOINT_NAME);
throw e;
} finally {
LOG.d("Releasing savepoint %s", USER_SYNC_SAVEPOINT_NAME);
dbTransactionHelper.releaseNamedTransaction(USER_SYNC_SAVEPOINT_NAME);
dbTransactionHelper.close();
client.release();
}
}
private Set<JsonUser> getUsersFromServer() throws ExecutionException, InterruptedException {
RequestFuture<List<JsonUser>> future = RequestFuture.newFuture();
App.getServer().listUsers(null, future, future);
List<JsonUser> users = future.get();
LOG.i("Got %d users from server", users.size());
return new HashSet<>(users);
}
/**
* Retrieves a user set. If there is no user or if an error occurs, then an unmodifiable empty
* set is returned.
* */
private Set<JsonUser> getUsersFromDb() {
Cursor cursor = null;
ContentProviderClient client = null;
try {
client = App.getInstance().getContentResolver()
.acquireContentProviderClient(Users.CONTENT_URI);
// Request users from database.
cursor = client.query(Users.CONTENT_URI, new String[]{Users.FULL_NAME, Users.UUID},
null, null, Users.FULL_NAME);
// If no data was retrieved from database
if (cursor == null || cursor.getCount() == 0) {
return ImmutableSet.of();
}
int fullNameColumn = cursor.getColumnIndex(Users.FULL_NAME);
int uuidColumn = cursor.getColumnIndex(Users.UUID);
Set<JsonUser> result = new HashSet<>();
while (cursor.moveToNext()) {
JsonUser user =
new JsonUser(cursor.getString(uuidColumn), cursor.getString(fullNameColumn));
result.add(user);
}
return result;
} catch (SQLiteException e) {
LOG.w(e, "Error retrieving users from database;");
return ImmutableSet.of();
} catch (RemoteException e) {
LOG.w(e, "Error retrieving users from database");
return ImmutableSet.of();
}finally {
if (cursor != null) {
cursor.close();
}
if (client != null) {
client.release();
}
}
}
/** Given a set of users, replaces the current set of users with users from that set. */
private static ArrayList<ContentProviderOperation> getUserUpdateOps(
Set<JsonUser> response, SyncResult syncResult) {
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
// Delete all users before inserting.
ops.add(ContentProviderOperation.newDelete(Contracts.Users.CONTENT_URI).build());
// TODO: Update syncResult delete counts.
for (JsonUser user : response) {
ops.add(ContentProviderOperation.newInsert(Contracts.Users.CONTENT_URI)
.withValue(Contracts.Users.UUID, user.id)
.withValue(Contracts.Users.FULL_NAME, user.fullName)
.build());
syncResult.stats.numInserts++;
}
return ops;
}
}