nheyek/SDP-2018

View on GitHub
app/src/main/java/ch/epfl/sweng/studyup/firebase/Firestore.java

Summary

Maintainability
C
1 day
Test Coverage
package ch.epfl.sweng.studyup.firebase;

import android.app.Activity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;

import com.alamkanak.weekview.WeekViewEvent;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreSettings;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import com.google.firebase.firestore.QuerySnapshot;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ch.epfl.sweng.studyup.player.Player;
import ch.epfl.sweng.studyup.questions.Question;
import ch.epfl.sweng.studyup.questions.QuestionParser;
import ch.epfl.sweng.studyup.utils.Callback;

import static ch.epfl.sweng.studyup.utils.Constants.Course;
import static ch.epfl.sweng.studyup.utils.Constants.FB_ANSWERED_QUESTIONS;
import static ch.epfl.sweng.studyup.utils.Constants.FB_COURSE;
import static ch.epfl.sweng.studyup.utils.Constants.FB_COURSES;
import static ch.epfl.sweng.studyup.utils.Constants.FB_COURSES_ENROLLED;
import static ch.epfl.sweng.studyup.utils.Constants.FB_COURSES_TEACHED;
import static ch.epfl.sweng.studyup.utils.Constants.FB_CURRENCY;
import static ch.epfl.sweng.studyup.utils.Constants.FB_EVENTS;
import static ch.epfl.sweng.studyup.utils.Constants.FB_FIRSTNAME;
import static ch.epfl.sweng.studyup.utils.Constants.FB_ITEMS;
import static ch.epfl.sweng.studyup.utils.Constants.FB_KNOWN_NPCS;
import static ch.epfl.sweng.studyup.utils.Constants.FB_LASTNAME;
import static ch.epfl.sweng.studyup.utils.Constants.FB_LEVEL;
import static ch.epfl.sweng.studyup.utils.Constants.FB_QUESTIONS;
import static ch.epfl.sweng.studyup.utils.Constants.FB_QUESTION_ANSWER;
import static ch.epfl.sweng.studyup.utils.Constants.FB_QUESTION_AUTHOR;
import static ch.epfl.sweng.studyup.utils.Constants.FB_QUESTION_CLICKEDINSTANT;
import static ch.epfl.sweng.studyup.utils.Constants.FB_QUESTION_DURATION;
import static ch.epfl.sweng.studyup.utils.Constants.FB_QUESTION_LANG;
import static ch.epfl.sweng.studyup.utils.Constants.FB_QUESTION_TITLE;
import static ch.epfl.sweng.studyup.utils.Constants.FB_QUESTION_TRUEFALSE;
import static ch.epfl.sweng.studyup.utils.Constants.FB_SCIPER;
import static ch.epfl.sweng.studyup.utils.Constants.FB_SPECIALQUESTS;
import static ch.epfl.sweng.studyup.utils.Constants.FB_TEACHING_STAFF;
import static ch.epfl.sweng.studyup.utils.Constants.FB_UNLOCKED_THEME;
import static ch.epfl.sweng.studyup.utils.Constants.FB_USERNAME;
import static ch.epfl.sweng.studyup.utils.Constants.FB_USERS;
import static ch.epfl.sweng.studyup.utils.Constants.FB_XP;
import static ch.epfl.sweng.studyup.utils.Constants.MOCK_UUIDS;
import static ch.epfl.sweng.studyup.utils.Constants.Role;
import static ch.epfl.sweng.studyup.utils.GlobalAccessVariables.DB_STATIC_INFO;
import static ch.epfl.sweng.studyup.utils.GlobalAccessVariables.MOCK_ENABLED;
import static ch.epfl.sweng.studyup.utils.Utils.getMapListFromSpecialQuestList;
import static ch.epfl.sweng.studyup.utils.Utils.getStringListFromCourseList;
import static ch.epfl.sweng.studyup.utils.Utils.waitAndTag;

/**
 * Firestore
 *
 * Our own Firebase Cloud Firestore API.
 */
public class Firestore {

    private static FirebaseFirestore db = FirebaseFirestore.getInstance();
    private static final String TAG = Firestore.class.getSimpleName();
    private static Firestore instance = null;

    private Firestore() {
        FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder()
                .setPersistenceEnabled(true)
                .setTimestampsInSnapshotsEnabled(true)
                .build();
        try { db.setFirestoreSettings(settings); } catch(Exception e) {}
    }

    public static Firestore get() {
        if (instance == null) {
            instance = new Firestore();
        }
        return instance;
    }

    public FirebaseFirestore getDb() {
        return db;
    }

    public void updateRemotePlayerDataFromLocal() {

        Player currPlayer = Player.get();
        Map<String, Object> localPlayerData = new HashMap<>();

        localPlayerData.put(FB_SCIPER, currPlayer.getSciperNum());
        localPlayerData.put(FB_FIRSTNAME, currPlayer.getFirstName());
        localPlayerData.put(FB_LASTNAME, currPlayer.getLastName());
        localPlayerData.put(FB_USERNAME, currPlayer.getUserName());
        localPlayerData.put(FB_XP, currPlayer.getExperience());
        localPlayerData.put(FB_CURRENCY, currPlayer.getCurrency());
        localPlayerData.put(FB_LEVEL, currPlayer.getLevel());
        localPlayerData.put(FB_ITEMS, currPlayer.getItemNames());
        localPlayerData.put(FB_SPECIALQUESTS, getMapListFromSpecialQuestList(currPlayer.getSpecialQuests()));
        localPlayerData.put(FB_COURSES_ENROLLED, getStringListFromCourseList(currPlayer.getCoursesEnrolled(), false));
        localPlayerData.put(FB_COURSES_TEACHED, getStringListFromCourseList(currPlayer.getCoursesTeached(), false));
        localPlayerData.put(FB_ANSWERED_QUESTIONS, currPlayer.getAnsweredQuestion());
        localPlayerData.put(FB_QUESTION_CLICKEDINSTANT, currPlayer.getClickedInstants());
        localPlayerData.put(FB_UNLOCKED_THEME, new ArrayList<>(currPlayer.getUnlockedThemes()));
        localPlayerData.put(FB_KNOWN_NPCS, new ArrayList<>(currPlayer.getKnownNPCs()));

        db.document(FB_USERS + "/" + currPlayer.getSciperNum())
                .set(localPlayerData)
                .addOnSuccessListener(new OnSuccessListener<Void>() {
                    @Override
                    public void onSuccess(Void aVoid) {
                        Log.i(TAG, "Remote player data was updated.");
                    }
                })
                .addOnFailureListener(new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        Log.e(TAG, "Unable to update remote player data");
                    }
                });

    }

    public void addQuestion(final Question question) {
        Map<String, Object> questionData = new HashMap<>();
        questionData.put(FB_QUESTION_TRUEFALSE, question.isTrueFalse());
        questionData.put(FB_QUESTION_ANSWER, question.getAnswer());
        questionData.put(FB_QUESTION_TITLE, question.getTitle());
        questionData.put(FB_COURSE, question.getCourseName());
        questionData.put(FB_QUESTION_AUTHOR, Player.get().getSciperNum());
        questionData.put(FB_QUESTION_LANG, question.getLang());
        questionData.put(FB_QUESTION_DURATION, question.getDuration());

        db.collection(FB_QUESTIONS).document(question.getQuestionId()).set(questionData);
    }

    /**
     * Version without callback of loadQuestions
     */
    public void loadQuestions(final Context context) throws NullPointerException {
        loadQuestions(context, null);
    }

    /**
     * Load all questions that have not been created by the current player if role is student.
     * Load all questions that have been created by the current player if the role is teacher.
     * In addition, only questions that correspond to the current player's courses should be loaded.
     *
     * @param context The context used to save the questions locally
     * @throws NullPointerException If the data received from the server is not of a valid format
     */
    public void loadQuestions(final Context context, final Callback onQuestionsLoaded) throws NullPointerException {

        final Player currPlayer = Player.get();

        final List<Question> questionList = new ArrayList<>();

        db.collection(FB_QUESTIONS).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
            @Override
            public void onComplete(@NonNull Task<QuerySnapshot> task) {
            if (task.isSuccessful()) {
                for (QueryDocumentSnapshot document : task.getResult()) {

                    String questionId = document.getId();
                    Map<String, Object> questionData = document.getData();

                    Question q = extractQuestionData(currPlayer, questionId, questionData);
                    if(q != null) questionList.add(q);
                }

                QuestionParser.writeQuestions(questionList, context);
                if(onQuestionsLoaded != null) {
                    onQuestionsLoaded.call(questionList);
                }
                Log.d(TAG, "Question List: " + questionList.toString());
            } else {
                Log.e(TAG, "Error getting documents: ", task.getException());
            }
            }
        });
    }


    private Question extractQuestionData(Player currPlayer, String id, Map<String, Object> questionData) {
        if(!MOCK_ENABLED && MOCK_UUIDS.contains(id)) return null;
        Course questionCourse = Course.valueOf(questionData.get(FB_COURSE).toString());
        List<Course> playerCourse = currPlayer.isTeacher() ? currPlayer.getCoursesTeached() : currPlayer.getCoursesEnrolled();
        boolean questionCourseMatchesPlayer = playerCourse.contains(questionCourse);

        if (questionCourseMatchesPlayer) {

            String questionTitle = (String) questionData.get(FB_QUESTION_TITLE);
            Boolean questionTrueFalse = (Boolean) questionData.get(FB_QUESTION_TRUEFALSE);
            int questionAnswer = Integer.parseInt((questionData.get(FB_QUESTION_ANSWER)).toString());
            String questionLang = questionData.get(FB_QUESTION_LANG).toString();
            long clickedInstant = getLongValueOrDefault(questionData, FB_QUESTION_CLICKEDINSTANT);
            if (clickedInstant != 0) {
                Player.get().addClickedInstant(id, clickedInstant);
            }
            long duration = getLongValueOrDefault(questionData, FB_QUESTION_DURATION);

            return new Question(id, questionTitle, questionTrueFalse, questionAnswer, questionCourse.name(), questionLang, duration);
        }

        return null;
    }

    private long getLongValueOrDefault(Map<String, Object> questionData, String fbParameter) {
        Object getFromFB = questionData.get(fbParameter);
        if (getFromFB == null) {
            return 0;
        } else {
            return Long.parseLong(getFromFB.toString());
        }
    }

    /**
     * This function put the current data of a given user into the dbStaticInfo object (Utils.java).
     *
     * @param sciper Sciper ot the user.
     */
    public void getData(final int sciper) {
        db.collection(FB_USERS).document(Integer.toString(sciper))
            .get()
            .addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
                @Override
                public void onSuccess(DocumentSnapshot document) {
                    if (document.exists()) {
                        DB_STATIC_INFO = document.getData();
                    }
                }
            })
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {Log.i(TAG, "Failed to load data for player with Sciper number: " + sciper); }});
    }

    public void getCoursesSchedule(final Activity act, final Role role) throws NullPointerException {
        FirestoreSchedule.getCoursesSchedule(db, act, role);
    }

    public void addPlayerToTeachingStaff(final Course c, final String sciper) {
        final DocumentReference courseRef = db.collection(FB_COURSES).document(c.name());
        courseRef.get()
                .addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<DocumentSnapshot> task) {
                        if (task.isSuccessful()) {
                            if(task.getResult().exists()) {
                                DocumentSnapshot doc = task.getResult();

                                List<String> teachers;
                                try {
                                    teachers = (List<String>) doc.getData().get(FB_TEACHING_STAFF);
                                } catch (ClassCastException e) {
                                    Log.d(TAG, "onComplete: The info for the teacher of " + c.name() + " is incorrect.");
                                    return;
                                }

                                if (!teachers.contains(sciper)) {
                                    Map<String, Object> courseData = doc.getData();
                                    teachers.add(sciper);
                                    courseData.put(FB_TEACHING_STAFF, teachers);
                                    courseRef.set(courseData);
                                }
                            } else {
                                Map<String, Object> courseData = new HashMap<>();
                                ArrayList<String> staff = new ArrayList<>(Arrays.asList(sciper));
                                courseData.put(FB_TEACHING_STAFF, staff);
                                courseRef.set(courseData);
                            }

                        } else {
                            Log.w(TAG, "The schedule fail to load or no course are present.");
                        }
                    }
                });
    }

    public void removePlayerFromTeachingStaff(final Course c, final String sciper) {
        final DocumentReference courseRef = db.collection(FB_COURSES).document(c.name());
        courseRef.get()
                .addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<DocumentSnapshot> task) {
                        if (task.isSuccessful() && task.getResult().exists()) {
                            DocumentSnapshot doc = task.getResult();

                            List<String> teachers;
                            try {
                                teachers = (List<String>) doc.getData().get(FB_TEACHING_STAFF);
                            } catch(ClassCastException e) { Log.d(TAG, "onComplete: The info for the teacher of "+c.name()+" is incorrect."); return; }

                            Map<String, Object> courseData = doc.getData();
                            if(teachers.remove(sciper) && teachers.isEmpty()) {
                                deleteCourseInfos(c);
                            } else {
                                courseData.put(FB_TEACHING_STAFF, teachers);
                                courseRef.set(courseData);
                            }
                        } else {
                            Log.w(TAG, "The schedule fail to load or no course are present.");
                        }
                    }
                });
    }

    private void resetCourseSchedule(final Course c) {
        DocumentReference courseRef = db.collection(FB_COURSES).document(c.name());
        courseRef.collection(FB_EVENTS).get()
                .addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
                    @Override
                    public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
                        for(final QueryDocumentSnapshot q : queryDocumentSnapshots) {
                            q.getReference().delete()
                                    .addOnFailureListener(new OnFailureListener() {
                                        @Override
                                        public void onFailure(@NonNull Exception e) {
                                            Log.i(TAG, "Failed during the deletion of period: \n"+q.toObject(WeekViewEvent.class));
                                        }
                                    });
                        }
                    }
                });
    }
    public void setCourseEvents(final Course c, final List<WeekViewEvent> periodsToAdd) {
        FirestoreSchedule.setCourseEvents(db, c, periodsToAdd);
    }

    public void deleteCourseInfos(Course c) {
        resetCourseSchedule(c); // To delete the periods on the server
        waitAndTag(1000, TAG);
        db.collection(FB_COURSES).document(c.name()).delete();
    }
}