app/src/main/java/ch/epfl/culturequest/database/Database.java
package ch.epfl.culturequest.database;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.GenericTypeIndicator;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.Transaction;
import com.google.firebase.database.ValueEventListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import ch.epfl.culturequest.BuildConfig;
import ch.epfl.culturequest.backend.artprocessing.processingobjects.BasicArtDescription;
import ch.epfl.culturequest.backend.tournament.tournamentobjects.ArtQuiz;
import ch.epfl.culturequest.backend.tournament.tournamentobjects.QuizQuestion;
import ch.epfl.culturequest.backend.tournament.tournamentobjects.Tournament;
import ch.epfl.culturequest.notifications.PushNotification;
import ch.epfl.culturequest.social.Follows;
import ch.epfl.culturequest.social.Post;
import ch.epfl.culturequest.social.Profile;
import ch.epfl.culturequest.social.SightseeingEvent;
/**
* This class is the implementation of the database using Firebase
*/
public class Database {
private static final FirebaseDatabase databaseInstance = FirebaseDatabase.getInstance();
private static boolean isEmulatorOn = false;
public static void setPersistenceEnabled() {
// !BuildConfig.IS_TESTING makes the code run only when we are not testing
if (!BuildConfig.IS_TESTING.get()) {
databaseInstance.setPersistenceEnabled(true);
databaseInstance.getReference("users").keepSynced(true);
databaseInstance.getReference("posts").keepSynced(true);
databaseInstance.getReference("follows").keepSynced(true);
databaseInstance.getReference("artworks").keepSynced(true);
databaseInstance.getReference("notifications").keepSynced(true);
databaseInstance.getReference("sightseeing_event").keepSynced(true);
databaseInstance.getReference("tournaments").keepSynced(true);
}
}
public static void setEmulatorOn() {
if (!isEmulatorOn) {
databaseInstance.useEmulator("10.0.2.2", 9000);
isEmulatorOn = true;
}
}
public static CompletableFuture<AtomicBoolean> clearDatabase() {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
if (isEmulatorOn) {
databaseInstance.getReference().setValue(null).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
return future;
}
return CompletableFuture.completedFuture(new AtomicBoolean(false));
}
private static <T> CompletableFuture<T> getValue(DatabaseReference ref, Class<T> valueType) {
CompletableFuture<T> future = new CompletableFuture<>();
ref.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
T value = task.getResult().getValue(valueType);
future.complete(value);
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
public static CompletableFuture<Profile> getProfile(String UId) {
DatabaseReference usersRef = databaseInstance.getReference("users").child(UId);
return getValue(usersRef, Profile.class);
}
public static CompletableFuture<List<Profile>> getAllProfiles() {
return getNumberOfProfiles().thenCompose(Database::getTopNProfiles);
}
public static CompletableFuture<AtomicBoolean> setProfile(Profile profile) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
databaseInstance.getReference("users").child(profile.getUid()).setValue(profile).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
return future;
}
public static CompletableFuture<AtomicBoolean> deleteProfile(String uid) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference ref = databaseInstance.getReference("users").child(uid);
ref.removeValue().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
return future;
}
public static CompletableFuture<AtomicBoolean> removeAllPosts(String uid) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference ref = databaseInstance.getReference("posts").child(uid);
ref.removeValue().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
return future;
}
/**
* @param UId the user's id
* @return the rank of the user in the database with respect to their score
*/
public static CompletableFuture<Integer> getRank(String UId) {
CompletableFuture<Integer> future = new CompletableFuture<>();
getAllProfiles().whenComplete((allProfiles, e) -> {
int rank = findRank(UId, allProfiles);
if (rank != -1) {
future.complete(rank);
} else {
future.completeExceptionally(new RuntimeException("User not found"));
}
});
return future;
}
/**
* @param UId the user's id
* @return the rank of the user in the database with respect to their score among his friends
*/
public static CompletableFuture<Integer> getRankFriends(String UId) {
CompletableFuture<Integer> future = new CompletableFuture<>();
getFollowed(UId).whenComplete((followed, e) -> {
if (followed == null || followed.getFollowed().isEmpty()) {
future.complete(1);
return;
}
System.out.println("Following " + followed.getFollowed().size());
getTopNFriendsProfiles(followed.getFollowed().size() + 1).whenComplete((friendsProfiles, e2) -> {
if (friendsProfiles == null) {
System.out.println("Error 1");
future.completeExceptionally(new RuntimeException("User not found"));
return;
}
int rank = findRank(UId, friendsProfiles);
if (rank != -1) {
future.complete(rank);
} else {
System.out.println("Error 2");
future.completeExceptionally(new RuntimeException("User not found in ranking"));
}
});
});
return future;
}
private static int findRank(String UId, List<Profile> profiles) {
int rank = 1;
for (Profile p : profiles) {
if (Objects.equals(p.getUid(), UId)) {
return rank;
}
rank++;
}
return -1;
}
/**
* @return the number of profiles in the database
*/
public static CompletableFuture<Integer> getNumberOfProfiles() {
DatabaseReference usersRef = databaseInstance.getReference("users");
CompletableFuture<Integer> future = new CompletableFuture<>();
usersRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete((int) task.getResult().getChildrenCount());
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
/**
* @param n the number of profiles to get
* @return the top n profiles in the database with respect to their score
*/
public static CompletableFuture<List<Profile>> getTopNProfiles(int n) {
DatabaseReference usersRef = databaseInstance.getReference("users");
CompletableFuture<List<Profile>> future = new CompletableFuture<>();
usersRef.orderByChild("score").limitToLast(n).get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
List<Profile> profilesList = new ArrayList<>();
for (DataSnapshot snapshot : task.getResult().getChildren()) {
Profile profile = snapshot.getValue(Profile.class);
profilesList.add(profile);
}
profilesList.sort((p1, p2) -> p2.getScore().compareTo(p1.getScore()));
future.complete(profilesList);
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
/**
* @param n the number of profiles to get
* @return the top n profiles in the database with respect to their score among the active user friends
*/
public static CompletableFuture<List<Profile>> getTopNFriendsProfiles(int n) {
DatabaseReference usersRef = databaseInstance.getReference("users");
CompletableFuture<List<Profile>> future = new CompletableFuture<>();
Profile.getActiveProfile().retrieveFriends().thenAccept(friends -> {
//the list of profiles to return (the top n profiles)
List<Profile> profilesList = new ArrayList<>();
//if the user does not have any friends, no need to fetch the database
if (friends == null || friends.isEmpty()) {
profilesList.add(Profile.getActiveProfile());
future.complete(profilesList);
}
usersRef.orderByChild("score").get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
for (DataSnapshot snapshot : task.getResult().getChildren()) {
Profile profile = snapshot.getValue(Profile.class);
if (profile != null && (friends.contains(profile.getUid()) || profile.getUid().equals(Profile.getActiveProfile().getUid()))) {
profilesList.add(profile);
}
}
List<Profile> topNProfiles = profilesList.subList(0, Math.min(n, profilesList.size()));
topNProfiles.sort((p1, p2) -> p2.getScore().compareTo(p1.getScore()));
future.complete(topNProfiles);
} else {
future.completeExceptionally(task.getException());
}
});
}).exceptionally(e -> {
future.completeExceptionally(e);
return null;
});
return future;
}
public static CompletableFuture<AtomicBoolean> updateScore(String uid, int newScore) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference scoreRef = databaseInstance.getReference("users/" + uid + "/score");
scoreRef.setValue(newScore, (error, ref) -> {
future.complete(new AtomicBoolean(error == null));
});
return future;
}
public static CompletableFuture<HashMap<String, Integer>> updateBadges(String uid, List<String> newbadges) {
CompletableFuture<HashMap<String, Integer>> future = new CompletableFuture<>();
DatabaseReference badgesRef = databaseInstance.getReference("users/" + uid + "/badges");
badgesRef.runTransaction(
new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData mutableData) {
HashMap<String, Integer> badges = mutableData.getValue(new GenericTypeIndicator<HashMap<String, Integer>>() {
});
if (badges == null) {
badges = new HashMap<>();
}
for (String badge : newbadges) {
if (badges.containsKey(badge)) {
badges.put(badge, badges.get(badge) + 1);
} else {
badges.put(badge, 1);
}
}
mutableData.setValue(badges);
return Transaction.success(mutableData);
}
@Override
public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) {
if (committed) {
HashMap<String, Integer> badges = dataSnapshot.getValue(new GenericTypeIndicator<HashMap<String, Integer>>() {
});
future.complete(badges);
} else {
future.completeExceptionally(databaseError.toException());
}
}
});
return future;
}
/**
* This method is used to upload a post to the database
*
* @param post the post to be uploaded
* @return a CompletableFuture that will be completed when the upload is done
*/
public static CompletableFuture<AtomicBoolean> uploadPost(Post post) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference usersRef = databaseInstance.getReference("posts").child(post.getUid()).child(String.valueOf(post.getPostId()));
usersRef.setValue(post).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
return future;
}
/**
* This method is used to remove a post from the database
*
* @param post the post to be removed
* @return a CompletableFuture that will be completed when the removal is done
*/
public static CompletableFuture<AtomicBoolean> removePost(Post post) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference usersRef = databaseInstance.getReference("posts").child(post.getUid()).child(post.getPostId());
usersRef.removeValue((error, ref1) -> {
if (error == null) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
return future;
}
/**
* This method is used to get the posts of a user
*
* @param UId the user's id
* @param limit the maximum number of posts to be returned
* @param offset the number of posts to be skipped
* @return a CompletableFuture that will be completed when the posts are retrieved
*/
public static CompletableFuture<List<Post>> getPosts(String UId, int limit, int offset) {
CompletableFuture<List<Post>> future = new CompletableFuture<>();
DatabaseReference postsRef = databaseInstance.getReference("posts").child(UId);
postsRef.orderByChild("time").limitToLast(limit + offset).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
List<Post> posts = new ArrayList<>();
for (DataSnapshot postSnapshot : dataSnapshot.getChildren()) {
Post post = postSnapshot.getValue(Post.class);
if (post != null) posts.add(post);
}
// Order posts by time descending
Collections.reverse(posts);
// Return only the requested number of posts
if (posts.size() > offset) {
posts = posts.subList(offset, posts.size());
} else {
posts = Collections.emptyList();
}
future.complete(posts);
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
future.completeExceptionally(databaseError.toException());
}
});
return future;
}
/**
* This method is used to get the posts of a user
*
* @param UId the user's id
*/
public static CompletableFuture<List<Post>> getPosts(String UId) {
CompletableFuture<List<Post>> future = new CompletableFuture<>();
DatabaseReference usersRef = databaseInstance.getReference("posts").child(UId);
usersRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
List<Post> posts = new ArrayList<>();
for (DataSnapshot snapshot : task.getResult().getChildren()) {
Post post = snapshot.getValue(Post.class);
posts.add(post);
}
// sort by time such that we get the latest posts first
posts.sort((p1, p2) -> Long.compare(p2.getTime(), p1.getTime()));
future.complete(posts);
} else {
future.complete(List.of());
}
});
return future;
}
/**
* This method is used to get the posts of a user's followings
*
* @param UIds the user's id
* @param limit the maximum number of posts to be returned
* @param offset the number of posts to be skipped
* @return a CompletableFuture that will be completed when the posts are retrieved
*/
public static CompletableFuture<List<Post>> getPostsFeed(List<String> UIds, int limit, int offset) {
//List of posts to retrieve from each user
CompletableFuture<List<Post>>[] futures = UIds.stream().map((uid) -> {
return getPosts(uid, limit, 0);
}).toArray(CompletableFuture[]::new);
//Future to return
CompletableFuture<List<Post>> result = new CompletableFuture<>();
//Wait for all futures to complete
CompletableFuture.allOf(futures).whenComplete((v, e) -> {
//Merge all posts into one list
List<Post> posts = new ArrayList<>();
for (CompletableFuture<List<Post>> future : futures) {
posts.addAll(future.join());
}
posts.sort(Comparator.comparing(Post::getTime).reversed());
posts = posts.subList(offset, Math.min(posts.size(), limit + offset));
result.complete(posts);
}).exceptionally(e -> {
result.complete(List.of());
return null;
});
return result;
}
/**
* This method is used to get the posts of a user's followings
*
* @param UIds the user's id
* @param limit the maximum number of posts to be returned
* @return a CompletableFuture that will be completed when the posts are retrieved
*/
public static CompletableFuture<List<Post>> getPostsFeed(List<String> UIds, int limit) {
return getPostsFeed(UIds, limit, 0);
}
/**
* This method is used to get the posts of a user's followings
*
* @param UIds the user's id
* @return a CompletableFuture that will be completed when the posts are retrieved
*/
public static CompletableFuture<List<Post>> getPostsFeed(List<String> UIds) {
return getPostsFeed(UIds, 100, 0);
}
/**
* This method is used to get the posts of a user's followings
*
* @param post the post to be liked
* @param UId the user's id
* @return a CompletableFuture that will be completed when the posts are retrieved
*/
public static CompletableFuture<Post> addLike(Post post, String UId) {
return changeLike(post, UId, true);
}
/**
* @param post the post to remove the like from
* @param UId the id of the user who liked the post
* @return a future that will return true if the like was removed successfully, false otherwise
*/
public static CompletableFuture<Post> removeLike(Post post, String UId) {
return changeLike(post, UId, false);
}
private static CompletableFuture<Post> changeLike(Post post, String UId, boolean add) {
CompletableFuture<Post> future = new CompletableFuture<>();
DatabaseReference usersRef = databaseInstance.getReference("posts").child(post.getUid()).child(post.getPostId());
usersRef.runTransaction(handler(future, UId, add));
return future;
}
private static Transaction.Handler handler(CompletableFuture<Post> future, String UId, boolean add) {
return new Transaction.Handler() {
@NonNull
@Override
public Transaction.Result doTransaction(@NonNull MutableData mutableData) {
Post dbPost = mutableData.getValue(Post.class);
if (dbPost == null) {
return Transaction.success(mutableData);
}
if (add) {
dbPost.addLike(UId);
} else {
dbPost.removeLike(UId);
}
mutableData.setValue(dbPost);
return Transaction.success(mutableData);
}
@Override
public void onComplete(@Nullable DatabaseError databaseError, boolean b, @Nullable DataSnapshot dataSnapshot) {
if (databaseError != null || dataSnapshot == null) {
future.complete(null);
} else {
future.complete(dataSnapshot.getValue(Post.class));
}
}
};
}
public static CompletableFuture<Follows> addFollow(String follower, String followed) {
return changeFollow(follower, followed, true);
}
public static CompletableFuture<Follows> removeFollow(String follower, String followed) {
return changeFollow(follower, followed, false);
}
private static CompletableFuture<Follows> changeFollow(String follower, String followed, boolean follow) {
CompletableFuture<Follows> future = new CompletableFuture<>();
DatabaseReference followsRef = databaseInstance.getReference("follows").child(follower);
followsRef.runTransaction(new Transaction.Handler() {
@NonNull
@Override
public Transaction.Result doTransaction(@NonNull MutableData currentData) {
Follows follows = currentData.getValue(Follows.class);
if (follows == null) {
follows = new Follows(new ArrayList<>());
}
if (follow) {
follows.addFollowed(followed);
} else {
follows.removeFollowed(followed);
}
currentData.setValue(follows);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
if (error != null) {
future.completeExceptionally(error.toException());
} else {
future.complete(currentData.getValue(Follows.class));
}
}
});
return future;
}
public static CompletableFuture<Follows> getFollowed(String UId) {
CompletableFuture<Follows> future = new CompletableFuture<>();
DatabaseReference followsRef = databaseInstance.getReference("follows").child(UId);
followsRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Follows follows = task.getResult().getValue(Follows.class);
if (follows == null) {
follows = new Follows(new ArrayList<>());
}
future.complete(follows);
} else {
future.complete(new Follows(new ArrayList<>()));
}
});
return future;
}
/**
* This method is used to retrieve an artwork when scanning. It is intentionally not completing
* the future if the artwork is not found, so that the api fetch can succeed before this one fails.
*
* @param artName the name of the artwork to be retrieved
* @return a CompletableFuture that will be completed when the artwork is retrieved
*/
public static CompletableFuture<BasicArtDescription> getArtworkScan(String artName) {
CompletableFuture<BasicArtDescription> future = new CompletableFuture<>();
DatabaseReference artRef = databaseInstance.getReference("artworks").child(artName);
artRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
BasicArtDescription art = task.getResult().getValue(BasicArtDescription.class);
if (art != null) {
future.complete(art);
}
}
});
return future;
}
/**
* This method is used to retrieve an artwork.
*
* @param artName the name of the artwork to be retrieved
* @return a CompletableFuture that will be completed when the artwork is retrieved, or will fail
* if the artwork is not found.
*/
public static CompletableFuture<BasicArtDescription> getArtwork(String artName) {
CompletableFuture<BasicArtDescription> future = new CompletableFuture<>();
DatabaseReference artRef = databaseInstance.getReference("artworks").child(artName);
artRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
BasicArtDescription art = task.getResult().getValue(BasicArtDescription.class);
if (art == null) {
future.completeExceptionally(new Exception("Artwork not found"));
} else {
future.complete(art);
}
} else {
future.completeExceptionally(new Exception("Artwork not found"));
}
});
return future;
}
public static CompletableFuture<AtomicBoolean> setArtwork(BasicArtDescription artworks) {
System.out.println("Setting artwork: " + artworks.getName());
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference artRef = databaseInstance.getReference("artworks").child(artworks.getName());
artRef.runTransaction(new Transaction.Handler() {
@NonNull
@Override
public Transaction.Result doTransaction(@NonNull MutableData currentData) {
if (currentData.getValue() == null) {
currentData.setValue(artworks);
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
if (error != null) {
future.completeExceptionally(error.toException());
} else {
future.complete(new AtomicBoolean(committed));
}
}
});
return future;
}
/**
* This method is used to set the device tokens of a user. It is used to update the list of
* devices to which the notifications of a user should be sent.
*
* @param UId of the user for which the device tokens are going to be set
* @param deviceTokens the list of device tokens to be set
* @return a CompletableFuture that will be completed with an AtomicBoolean when the device tokens are set
*/
public static CompletableFuture<AtomicBoolean> setDeviceTokens(String UId, List<String> deviceTokens) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
databaseInstance.getReference("users").child(UId).child("deviceTokens").setValue(deviceTokens).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
/**
* This method is used to get the device tokens of a user. It is used to know to which devices
* of a user a notification should be sent.
*
* @param UId of the user for which the device tokens are going to be retrieved
* @return a CompletableFuture that will be completed with a list of device tokens when the device tokens are retrieved
*/
public static CompletableFuture<List<String>> getDeviceTokens(String UId) {
CompletableFuture<List<String>> future = new CompletableFuture<>();
databaseInstance.getReference("users").child(UId).child("deviceTokens").get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
List<String> deviceTokens = new ArrayList<>();
for (DataSnapshot token : task.getResult().getChildren()) {
deviceTokens.add(token.getValue(String.class));
}
future.complete(deviceTokens);
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
/**
* This method is used to get the notifications of a user, ordered in reverse chronological order (i.e
* the most recent notification is the first one in the list). It is used to display the notifications
* in the app.
*
* @param UId of the user for which the notifications are going to be retrieved
* @return a CompletableFuture that will be completed with a list of notifications when the notifications are retrieved
*/
public static CompletableFuture<List<PushNotification>> getNotifications(String UId) {
CompletableFuture<List<PushNotification>> future = new CompletableFuture<>();
databaseInstance.getReference("notifications").child(UId).get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
List<PushNotification> notificationsList = new ArrayList<>();
for (DataSnapshot snapshot : task.getResult().getChildren()) {
notificationsList.add(snapshot.getValue(PushNotification.class));
}
// sort by time such that we get the latest notification first
notificationsList.sort((p1, p2) -> Long.compare(p2.getTime(), p1.getTime()));
future.complete(notificationsList);
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
/**
* This method is used to add a notification to a user. It is used when someone sends a
* notification to a user.
*
* @param UId of the user for which the notification is going to be added
* @param notification the notification to be added
* @return a CompletableFuture that will be completed with an AtomicBoolean when the notification is added
*/
public static CompletableFuture<AtomicBoolean> addNotification(String UId, PushNotification notification) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference notificationRef = databaseInstance.getReference("notifications").child(UId).child(notification.getNotificationId());
notificationRef.setValue(notification).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
/**
* This method is used to delete a notification from a user. It is used to delete old notifications.
*
* @param UId of the user for which the notification is going to be deleted
* @param notification the notification to be deleted
* @return a CompletableFuture that will be completed with an AtomicBoolean when the notification is deleted
*/
public static CompletableFuture<AtomicBoolean> deleteNotification(String UId, PushNotification notification) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference notificationRef = databaseInstance.getReference("notifications").child(UId).child(notification.getNotificationId());
notificationRef.removeValue().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
return future;
}
public static CompletableFuture<AtomicBoolean> setSightseeingEvent(SightseeingEvent event) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference usersRef = databaseInstance.getReference("sightseeing_event");
//// we need two separate set operations. if we merge both the owner also receives a notification...
usersRef.child(event.getOwner().getUid()).child(String.valueOf(event.getEventId())).setValue(event).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
event.getInvited().forEach(profile -> {
usersRef.child(profile.getUid()).child(String.valueOf(event.getEventId())).setValue(event).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
});
return future;
}
/**
* Returns the event or events user with Uid is invited to/organizes
*
* @param Uid user id
* @return the event or events organized by someone
*/
public static CompletableFuture<List<SightseeingEvent>> getSightseeingEvents(String Uid) {
CompletableFuture<List<SightseeingEvent>> future = new CompletableFuture<>();
DatabaseReference usersRef = databaseInstance.getReference("sightseeing_event").child(Uid);
usersRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
List<SightseeingEvent> events = new ArrayList<>();
for (DataSnapshot snapshot : task.getResult().getChildren()) {
SightseeingEvent event = snapshot.getValue(SightseeingEvent.class);
events.add(event);
}
future.complete(events);
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
public static CompletableFuture<AtomicBoolean> deleteSightseeingEvent(String Uid, String eventId) {
CompletableFuture<AtomicBoolean> future = new CompletableFuture<>();
DatabaseReference usersRef = databaseInstance.getReference("sightseeing_event").child(Uid).child(String.valueOf(eventId));
usersRef.removeValue().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(new AtomicBoolean(true));
} else {
future.complete(new AtomicBoolean(false));
}
});
return future;
}
public static void startQuiz(String tournament, String artName, String uid) {
setScoreQuiz(tournament, artName, uid, 0);
}
public static void setScoreQuiz(String tournament, String artName, String uid, int score) {
DatabaseReference quizRef = databaseInstance.getReference("tournaments").child(tournament).child("artQuizzes").child(artName).child("scores").child(uid);
quizRef.setValue(score);
}
public static CompletableFuture<Integer> getScoreQuiz(String tournament, String artName, String uid) {
DatabaseReference quizRef = databaseInstance.getReference("tournaments").child(tournament).child("artQuizzes").child(artName).child("scores").child(uid);
CompletableFuture<Integer> future = new CompletableFuture<>();
quizRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
future.complete(task.getResult().getValue(Integer.class));
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
public static CompletableFuture<Map<Profile, Integer>> getLeaderboard(Tournament tournament) {
CompletableFuture<Map<Profile, Integer>> future = new CompletableFuture<>();
DatabaseReference tournamentRef = databaseInstance.getReference("tournaments").child(tournament.getTournamentId());
tournamentRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Tournament t = task.getResult().getValue(Tournament.class);
if (t == null) {
future.completeExceptionally(new Exception("Tournament not found"));
return;
}
Map<String, Integer> leaderboard = new HashMap<>();
for (ArtQuiz quizz : t.getArtQuizzes().values()) {
for (Map.Entry<String, Integer> score : quizz.getScores().entrySet()) {
leaderboard.put(score.getKey(), leaderboard.getOrDefault(score.getKey(), 0) + score.getValue());
}
}
CompletableFuture<Profile>[] futures = leaderboard.keySet().stream().map((uid) -> getProfile(uid)).toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures).whenComplete((v, e) -> {
if (e != null) {
future.completeExceptionally(e);
return;
}
//Merge all posts into one list
List<Profile> profiles = new ArrayList<>();
for (CompletableFuture<Profile> futureProfile : futures) {
profiles.add(futureProfile.join());
}
Map<Profile, Integer> leaderboardWithProfiles = new HashMap<>();
for (Profile profile : profiles) {
leaderboardWithProfiles.put(profile, leaderboard.get(profile.getUid()));
}
future.complete(leaderboardWithProfiles);
}).exceptionally(e -> {
System.out.println("getLeaderboard failed: " + e.getMessage());
future.completeExceptionally(e);
return null;
});
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
public static CompletableFuture<String> getImageForArt(String artwork) {
CompletableFuture<String> future = new CompletableFuture<>();
// get the first post with "artworkName" = artwork
DatabaseReference postsRef = databaseInstance.getReference("posts");
postsRef.get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
//iterate over the users
for (DataSnapshot snapshot : task.getResult().getChildren()) {
//iterate over the posts
for (DataSnapshot post : snapshot.getChildren()) {
if (post.child("artworkName").getValue().equals(artwork)) {
future.complete(post.child("imageUrl").getValue().toString());
return;
}
}
}
future.completeExceptionally(new Exception("No post found for artwork " + artwork));
} else {
future.completeExceptionally(task.getException());
}
});
return future;
}
///// TOURNAMENTS //////
// Indicate other users that the tournament is currently being generated
public static CompletableFuture<Boolean> lockTournamentGeneration() {
DatabaseReference pathToGenerationLock = getDeviceSynchronizationRef().child("generationLocked");
return setBoolAsync(pathToGenerationLock, true);
}
// To unlock when the tournament is over or if one of the device fails to generate the tournament
public static CompletableFuture<Boolean> unlockTournamentGeneration() {
DatabaseReference pathToGenerationLock = getDeviceSynchronizationRef().child("generationLocked");
return setBoolAsync(pathToGenerationLock, false);
}
// Allow of form of synchronization to prevent other devices from generating the tournament if one of the device has already been charged to do so
public static CompletableFuture<Boolean> isTournamentGenerationLocked() {
DatabaseReference pathToGenerationLock = getDeviceSynchronizationRef().child("generationLocked");
return isEqualAsync(pathToGenerationLock, true);
}
// Indicate other devices that the tournament can now be fetched from Firebase
public static CompletableFuture<Boolean> indicateTournamentGenerated() {
DatabaseReference pathToGenerated = getDeviceSynchronizationRef().child("generated");
return setBoolAsync(pathToGenerated, true);
}
// Reset the generation state of the tournament to allow upcoming generation in the next week
public static CompletableFuture<Boolean> indicateTournamentNotGenerated() {
DatabaseReference pathToGenerated = getDeviceSynchronizationRef().child("generated");
return setBoolAsync(pathToGenerated, false);
}
// Slot where the variables used to handle the android apps synchronization are stored
public static DatabaseReference getDeviceSynchronizationRef() {
return databaseInstance.getReference("tournaments").child("device-synchronization");
}
// Put boolean in database reference and returns a future boolean indicating whether the operation was successful or not
// true -> setValue succeeded; null -> setValue failed
@SuppressLint("NewApi")
public static CompletableFuture<Boolean> setBoolAsync(DatabaseReference databaseReference, Boolean bool) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
databaseReference.setValue(bool, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(@Nullable DatabaseError error, @NonNull DatabaseReference ref) {
if (error != null) {
future.complete(null);
} else {
future.complete(true);
}
}
});
handleFutureTimeout(future, 120);
return future;
}
@SuppressLint("NewApi")
public static CompletableFuture<Boolean> isEqualAsync(DatabaseReference databaseReference, Boolean expectedBool) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
databaseReference.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
Boolean value = snapshot.getValue(Boolean.class);
if (value == null) {
future.complete(null);
} else {
future.complete(value.equals(expectedBool));
}
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
future.complete(null);
}
});
handleFutureTimeout(future, 120);
return future;
}
public static <T> void handleFutureTimeout(CompletableFuture<T> future, int timeoutSeconds) {
// create scheduled executor service
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.schedule(() -> {
if (!future.isDone()) {
future.completeExceptionally(new Exception("Timeout exception"));
}
}, timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS);
}
public static CompletableFuture<Void> uploadTournamentToDatabase(Tournament tournament) {
CompletableFuture<Void> voidFuture = new CompletableFuture<>();
DatabaseReference tournamentRef = databaseInstance.getReference().child("tournaments").child(tournament.getTournamentId());
auxiliaryUploadTournamentToDatabase(tournament, tournamentRef, 0, voidFuture);
return voidFuture;
}
private static void auxiliaryUploadTournamentToDatabase(Tournament tournament, DatabaseReference tournamentRef, int tentativeNumber, CompletableFuture<Void> voidFuture) {
tournamentRef.setValue(tournament, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(@Nullable DatabaseError error, @NonNull DatabaseReference ref) {
if (error != null) {
if (tentativeNumber == 2) {
// If the upload failed 2 times, we unlock the tournament generation so that another device can try to generate it
unlockTournamentGeneration();
voidFuture.completeExceptionally(new RuntimeException("Failed to upload tournament to database: " + error.getMessage()));
return;
}
// If the upload failed and tentative didn't reach max, try again
auxiliaryUploadTournamentToDatabase(tournament, tournamentRef, tentativeNumber + 1, voidFuture);
} else {
// If the upload is successful, we tell the other devices that the tournament can be fetched from the database
indicateTournamentGenerated();
voidFuture.complete(null);
}
}
});
}
public static CompletableFuture<Tournament> waitForTournamentGenerationAndFetchIt(AtomicReference<Tournament> fetchedTournament, CompletableFuture<Tournament> future, String tournamentId) {
DatabaseReference dbRef = databaseInstance.getReference();
DatabaseReference tournamentRef = dbRef.child("tournaments").child(tournamentId);
DatabaseReference generatedRef = getDeviceSynchronizationRef().child("generated");
generatedRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
boolean isGenerated = dataSnapshot.exists() ? dataSnapshot.getValue(Boolean.class) : false;
if (isGenerated) {
tournamentRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
fetchedTournament.set(dataSnapshot.getValue(Tournament.class));
future.complete(fetchedTournament.get());
}
@Override
public void onCancelled(DatabaseError databaseError) {
// todo: handle it better
future.completeExceptionally(new RuntimeException("Failed to read data from Firebase: " + databaseError.getMessage()));
}
});
}
}
@Override
public void onCancelled(DatabaseError databaseError) {
// todo: handle it better
future.completeExceptionally(new RuntimeException("Failed to read data from Firebase: " + databaseError.getMessage()));
}
});
return future;
}
public static CompletableFuture<Long> fetchSeedIfAlreadyGenerated() {
CompletableFuture<Long> seedFuture = new CompletableFuture<>();
DatabaseReference seedReference = databaseInstance.getReference().child("tournaments").child("device-synchronization").child("seed");
seedReference.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
Long seed = dataSnapshot.getValue(Long.class);
if (seed == null) {
seedFuture.complete(null);
} else {
seedFuture.complete(seed);
}
}
@Override
public void onCancelled(DatabaseError databaseError) {
seedFuture.completeExceptionally(new RuntimeException("Failed to read data from Firebase: " + databaseError.getMessage()));
}
});
return seedFuture;
}
public static CompletableFuture<Void> uploadSeedToDatabase(Long seed) {
CompletableFuture<Void> voidFuture = new CompletableFuture<>();
DatabaseReference seedReference = databaseInstance.getReference().child("tournaments").child("device-synchronization").child("seed");
seedReference.setValue(seed, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(@Nullable DatabaseError error, @NonNull DatabaseReference ref) {
if (error != null) {
voidFuture.completeExceptionally(new RuntimeException("Failed to upload seed to database: " + error.getMessage()));
} else {
voidFuture.complete(null);
}
}
});
return voidFuture;
}
}