whitewind664/sdp

View on GitHub
app/src/main/java/com/github/gogetters/letsgo/database/user/LetsGoUser.kt

Summary

Maintainability
A
0 mins
Test Coverage
A
99%
package com.github.gogetters.letsgo.database.user

import android.util.Log
import com.github.gogetters.letsgo.database.CloudStorage
import com.github.gogetters.letsgo.database.Database
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import com.google.firebase.database.DataSnapshot
import java.io.Serializable
import java.util.*
import kotlin.collections.ArrayList


/**
 * Class that represents a user of the app and coordinates the communication of user data with the
 * database.
 */
class LetsGoUser (val uid: String) : Serializable {

    companion object {
        // Be very careful if changing path values!
        private const val TAG = "FirestoreTest"
        private const val USERS_PATH = "users"
        private const val FRIEND_CHILD_PATH = "friends"
    }
    private val userPath = "$USERS_PATH/$uid"
    private val userFriendsPath = "$userPath/$FRIEND_CHILD_PATH"

    var nick: String? = null
    var first: String? = null
    var last: String? = null
    var city: String? = null
    var country: String? = null

    // The reference (== address) of the profile picture on cloud storage
    var profileImageRef: String? = null

    private var friendsByStatus: EnumMap<FriendStatus, MutableList<LetsGoUser>>? = null
    private var friendStatusMap: Map<LetsGoUser, FriendStatus>? = null

    // map related values
    var isLookingForPlayers: Boolean? = false
    var lastPositionLatitude: Double? = null
    var lastPositionLongitude: Double? = null

    /**
     * Verifies if the user exists in our database
     */
    fun requireUserExists(): Task<Unit> {
        return Database.readData(userPath).continueWith {
            val userExists = it.result.value != null

            if (userExists) {
                Log.d(TAG, "User exists! uid: $uid")
            } else {
                Log.e(TAG, "User DOESN'T EXIST! uid: $uid")
                throw IllegalStateException("User DOESN'T EXIST! uid: $uid")
            }
        }
    }

    /**
     * Uploads this User's data to the DB. Returns a task to track progress and etc.
     */
    fun uploadUserData(): Task<Void> {
        val userData = hashMapOf(
            "id" to uid,
            "nick" to nick,
            "first" to first,
            "last" to last,
            "city" to city,
            "country" to country,
            "profilePictureRef" to profileImageRef,
            "isLookingForPlayers" to isLookingForPlayers,
            "lastPositionLatitude" to lastPositionLatitude,
            "lastPositionLongitude" to lastPositionLongitude
        )

        // Add a new document with user's uid
        return Database.updateData(userPath, userData)
            .addOnSuccessListener {
                Log.d(TAG, "LetsGoUser document added for uid: $uid")
            }.addOnFailureListener { e -> Log.w(TAG, "Error adding LetsGoUser document", e) }
    }

    /**
     * Downloads this User's data to the DB. Returns a task to track progress and etc.
     */
    fun downloadUserData(): Task<Unit> {
        return Database.readData(userPath)
            .continueWith {
                extractUserData(it.result)
            }
            .addOnSuccessListener {
                Log.d(TAG, "LetsGoUser successfully downloaded: ${toString()}")
            }.addOnFailureListener { e -> Log.w(TAG, "Error downloading LetsGoUser for uid: $uid", e) }
    }

    private fun extractUserData(userData: DataSnapshot) {
        for (attribute in userData.children) {
            when (attribute.key) {
                "nick" -> nick = attribute.value as String
                "first" -> first = attribute.value as String
                "last" -> last = attribute.value as String
                "city" -> city = attribute.value as String
                "country" -> country = attribute.value as String
                "profilePictureRef" -> profileImageRef = attribute.value as String
                "isLookingForPlayers" -> isLookingForPlayers = attribute.value as Boolean
                "lastPositionLatitude" -> lastPositionLatitude = attribute.value as Double
                "lastPositionLongitude" -> lastPositionLongitude = attribute.value as Double
            }
        }
    }

    /**
     * Deletes this User's data from the DB. Returns a task to track progress and etc.
     */
    fun deleteUserData(): Task<Void> {
        // delete the profile picture
        if (profileImageRef != null) {
            CloudStorage.deleteFile(profileImageRef!!)
        }
        return Database.deleteData(userPath)
            .addOnSuccessListener {
                Log.d(TAG, "LetsGoUser successfully deleted!")
                nick = null
                first = null
                last = null
                city = null
                country = null
                isLookingForPlayers = null
                lastPositionLongitude = null
                lastPositionLatitude = null
                profileImageRef = null
            }.addOnFailureListener { e -> Log.w(TAG, "Error deleting LetsGoUser", e) }
    }

    override fun equals(other: Any?): Boolean {
        if (other is LetsGoUser) {
            return uid == other.uid
        }
        return false
    }

    override fun hashCode(): Int {
        return uid.hashCode()
    }

    override fun toString(): String {
        return "LetsGoUser(uid=$uid, nick=$nick, first=$first, last=$last, city=$city, country=$country, profileImageRef=$profileImageRef, isLookingForPlayers=$isLookingForPlayers," +
                "lastPositionLatitude=$lastPositionLatitude, lastPositionLongitude=$lastPositionLongitude)"
    }

    //===========================================================================================
    // Friend System

    enum class FriendStatus {
        /**
         * User has sent a friend request and is awaiting a response
         */
        SENT,

        /**
         * User has received a friend request and can respond by either ignoring or accepting
         */
        REQUESTED,

        /**
         * Users are friends! yay :)
         */
        ACCEPTED
    }

    /**
     * Send friend request to user
     */
    fun requestFriend(otherUser: LetsGoUser): Task<Void> {
        return updateFriendStatus(otherUser, FriendStatus.SENT, FriendStatus.REQUESTED)
    }

    /**
     * Accept friend request from a user
     */
    fun acceptFriend(otherUser: LetsGoUser): Task<Void> {
        return updateFriendStatus(otherUser, FriendStatus.ACCEPTED, FriendStatus.ACCEPTED)
    }

    /**
     * Use to remove a friend or delete a friend request.
     */
    fun deleteFriend(otherUser: LetsGoUser): Task<Void> {
        return Database.deleteData("$userFriendsPath/${otherUser.uid}").continueWithTask {
            Database.deleteData("${otherUser.userFriendsPath}/${this.uid}")
        }.addOnSuccessListener {
            Log.d(TAG, "'Friend' successfully deleted")
        }.addOnFailureListener { Log.d(TAG, "'Friend' FAILED to be deleted") }
    }

    /**
     * Gets the friend status of another user. Returns null if the users have no friend info stored.
     */
    fun getFriendStatus(otherUser: LetsGoUser): FriendStatus? {
        return friendStatusMap!![otherUser]
    }

    /**
     * Updates friend status for both users!
     */
    private fun updateFriendStatus(
        otherUser: LetsGoUser,
        status1: FriendStatus,
        status2: FriendStatus
    ): Task<Void> {
        return requireUserExists().continueWithTask {
            otherUser.requireUserExists().continueWithTask {
                updateFriendStatusHelper(otherUser, status1).continueWithTask {
                    otherUser.updateFriendStatusHelper(this, status2)
                }
            }
        }.addOnSuccessListener {
            Log.d(TAG, "Friend Status successfully updated")
        }.addOnFailureListener { Log.d(TAG, "Friend Status FAILED to be updated") }
    }

    /**
     * Updates otherUsers friend status in this users friend list!
     */
    private fun updateFriendStatusHelper(otherUser: LetsGoUser, status: FriendStatus): Task<Void> {
        val path = "$userFriendsPath/${otherUser.uid}"
        Log.d(TAG, "Adding friend data. path: $path\tstatus: $status")

        return Database.writeData(path, status.name);
    }

    //-------------------------------------------------------------------------------------------
    // Listing friends of a user

    /**
     * List pending friends, sent friend requests or current friends of User!
     */
    fun listFriendsByStatus(status: FriendStatus): List<LetsGoUser> {
        if (friendsByStatus == null) {
            throw IllegalStateException(
                "MUST call downloadFriends and wait for it to complete" +
                        " before calling this function!"
            )
        }
        return friendsByStatus!![status]!!
    }

    /**
     * Downloads Friends of all statuses.
     */
    // Sometimes I use the word connections and friends interchangeably
    fun downloadFriends(): Task<Unit> {
        return Database.readData(userFriendsPath).continueWithTask {
            val friendUids: EnumMap<FriendStatus, ArrayList<String>> =
                EnumMap(FriendStatus::class.java)
            friendsByStatus = EnumMap(FriendStatus::class.java)

            // Initializing lists in connections
            for (friendStatus in FriendStatus.values()) {
                friendUids[friendStatus] = ArrayList()
                friendsByStatus!![friendStatus] = ArrayList()
            }

            // Putting connection uids into their respective list
            for (connectionData in it.result.children) {
                val friendStatus = FriendStatus.valueOf(connectionData.value as String)
                val uid = connectionData.key

                friendUids[friendStatus]!!.add(uid!!)
            }

            // Turning a list of uids into a list of downloaded LetsGoUsers
            val tasks = ArrayList<Task<*>>()
            for (friendStatus in FriendStatus.values()) {
                tasks.add(
                    downloadUserList(friendUids[friendStatus]!!).continueWith {
                        friendsByStatus!![friendStatus] = it.result
                    }
                )
            }
            Tasks.whenAll(tasks).continueWith {
                fillFriendStatusPairs()
            }
        }
    }

    private fun fillFriendStatusPairs() {
        val temp : MutableMap<LetsGoUser, FriendStatus> = mutableMapOf()
        for (friendStatus in FriendStatus.values()) {
            for (friend in friendsByStatus!![friendStatus]!!) {
                temp[friend] = friendStatus
            }
        }
        friendStatusMap = temp
    }

    /**
     * Given a list of uids. Creates a list of LetsGoUsers and downloads their data
     */
    fun downloadUserList(uids: List<String>): Task<MutableList<LetsGoUser>> {
        val tasks = ArrayList<Task<LetsGoUser>>()

        for (uid in uids) {
            val user = LetsGoUser(uid)
            tasks.add(user.downloadUserData().continueWith { user })
        }

        return Tasks.whenAllSuccess(tasks)
    }


    //-------------------------------------------------------------------------------------------
    // User Search

    /**
     * Download all users who's nickname start with the given argument
     */
    fun downloadUsersByNick(nick: String): Task<MutableList<LetsGoUser>> {
        return Database.readSearchByChild(USERS_PATH, "nick", nick).continueWithTask {
            val uids = it.result.children.map { userData ->
                userData.key!! }.toList()
            downloadUserList(uids)
        }
    }

    //-------------------------------------------------------------------------------------------
    // User Search

    /**
     * Activates the location sharing and sends location to database.
     * Returns true when the data has been sent to the database
     */
    fun shareLocation(location: LatLng): Task<Void> {
        isLookingForPlayers = true
        lastPositionLatitude = location.latitude
        lastPositionLongitude = location.longitude
        return uploadUserData()
    }

    /**
     * Stops the displaying of the position with other users (not looking for a new game anymore)
     */
    fun disableLocationSharing(): Task<Void> {
        isLookingForPlayers = false
        return uploadUserData()
    }
}