asfoury/projmag

View on GitHub
app/src/main/java/com/sdp13epfl2021/projmag/activities/ProjectInformationActivity.kt

Summary

Maintainability
A
0 mins
Test Coverage
B
89%
package com.sdp13epfl2021.projmag.activities


import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isInvisible
import com.google.common.io.Files
import com.google.firebase.dynamiclinks.ktx.androidParameters
import com.google.firebase.dynamiclinks.ktx.dynamicLink
import com.google.firebase.dynamiclinks.ktx.dynamicLinks
import com.google.firebase.ktx.Firebase
import com.sdp13epfl2021.projmag.MainActivity
import com.sdp13epfl2021.projmag.R
import com.sdp13epfl2021.projmag.activities.ProjectCreationActivity.Companion.EDIT_EXTRA
import com.sdp13epfl2021.projmag.database.interfaces.*
import com.sdp13epfl2021.projmag.model.Candidature
import com.sdp13epfl2021.projmag.model.ImmutableProject
import com.sdp13epfl2021.projmag.video.VideoUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.glxn.qrgen.android.QRCode
import org.xml.sax.XMLReader
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
import javax.inject.Named
import kotlin.collections.ArrayList


/**
 * Activity displaying the information and media of a project and from which
 * one can apply to the project or send it using a deep link or QR code.
 */
@AndroidEntryPoint
class ProjectInformationActivity : AppCompatActivity() {

    @Inject
    lateinit var fileDB: FileDatabase

    @Inject
    lateinit var metadataDB: MetadataDatabase

    @Inject
    @Named("currentUserId")
    lateinit var userId: String

    @Inject
    lateinit var userdataDatabase: UserdataDatabase

    @Inject
    lateinit var projectDatabase: ProjectDatabase

    @Inject
    lateinit var candidatureDatabase: CandidatureDatabase

    private lateinit var projectVar: ImmutableProject
    private lateinit var videoView: VideoView
    private lateinit var descriptionView: TextView
    private lateinit var projectDir: File
    private lateinit var favButton: Button
    private val videosUris: MutableList<Pair<Uri, String?>> = ArrayList()
    private var current: Int = -1
    private var alreadyApplied: Boolean = false
    private var appliedProjectsIds: MutableList<ProjectId> = ArrayList()


    @Synchronized
    private fun addVideo(videoUri: Uri, subtitle: String?) {
        videosUris.add(Pair(videoUri, subtitle))
        if (current < 0) {
            readNextVideo()
            videoView.isInvisible = false
            videoView.pause()
        }
    }

    private fun readNextVideo() {
        if (current + 1 < videosUris.size) {
            current++
            updateVideoState()
        }
    }

    private fun readPrevVideo() {
        if (current > 0) {
            current--
            updateVideoState()
        }
    }

    private fun updateVideoState() {
        videoView.setVideoURI(videosUris[current].first)
        videosUris[current].second?.let {
            videoView.addSubtitleSource(
                it.byteInputStream(),
                VideoUtils.ENGLISH_WEBVTT_SUBTITLE_FORMAT
            )
        }
        videoView.start()
    }

    /**
     * Function that given a boolean and strings set the value of the button according to the string
     * values. When the boolean is null it shows loading by default.
     *
     * @param button button to manipulate
     * @param isOn boolean value indicating the value of the boolean
     * @param trueText text to show on the button when the boolean value is true
     * @param falseText text to show on the button when the boolean value is false
     */
    private fun setButtonText(
        button: Button, isOn: Boolean?,
        trueText: String, falseText: String
    ) {
        button.text = when (isOn) {
            null -> {
                button.isEnabled = false
                getString(R.string.loading)
            }
            true -> {
                button.isEnabled = true
                trueText
            }
            false -> {
                button.isEnabled = true
                falseText
            }
        }
    }

    private fun onApplyClick(
        applyButton: Button,
        candidatureDatabase: CandidatureDatabase,
        projectId: ProjectId,
    ) {
        if (alreadyApplied) {
            candidatureDatabase.removeCandidature(
                projectId,
                userId,
                {
                    showToast(getString(R.string.success), Toast.LENGTH_SHORT)
                    alreadyApplied = !alreadyApplied
                    setButtonText(
                        applyButton,
                        alreadyApplied,
                        getString(R.string.unaply_text),
                        getString(R.string.apply_text)
                    )
                },
                { showToast(getString(R.string.failure), Toast.LENGTH_SHORT) }
            )
        } else {
            candidatureDatabase.pushCandidature(
                projectId,
                userId,
                Candidature.State.Waiting,
                {
                    showToast(getString(R.string.success), Toast.LENGTH_SHORT)
                    alreadyApplied = !alreadyApplied
                    setButtonText(
                        applyButton,
                        alreadyApplied,
                        getString(R.string.unaply_text),
                        getString(R.string.apply_text)
                    )
                },
                { showToast(getString(R.string.failure), Toast.LENGTH_SHORT) }
            )
        }
    }

    private fun setUpApplyButton(applyButton: Button) {
        val projectId = projectVar.id
        setButtonText(
            applyButton,
            null,
            getString(R.string.unaply_text),
            getString(R.string.apply_text)
        )
        userdataDatabase.getListOfAppliedToProjects({ projectIds ->
            appliedProjectsIds.addAll(projectIds)
            val alreadyApplied = projectIds.contains(projectId)
            setButtonText(
                applyButton,
                alreadyApplied,
                getString(R.string.unaply_text),
                getString(R.string.apply_text)
            )
            applyButton.isEnabled = !projectVar.isTaken

        }, {})

        applyButton.setOnClickListener {
            userdataDatabase.applyUnapply(
                !alreadyApplied,
                projectId,
                {
                    if (userId.isNotEmpty()) {
                        onApplyClick(applyButton, candidatureDatabase, projectId)
                    } else {
                        showToast(getString(R.string.failure), Toast.LENGTH_SHORT)
                    }
                },
                { showToast(getString(R.string.failure), Toast.LENGTH_SHORT) }

            )
        }
    }

    private fun setUpFavoritesButton() {
        val projectId = projectVar.id

        //until data is loaded from database, show loading
        setButtonText(
            favButton,
            null,
            getString(R.string.favorite_remove_button),
            getString(R.string.favorite_add_button)
        )

        //load data from the database and set the favorite add button to the right value
        handleFavoriteButtonText(projectId, false)


        //handle click on the add favorite button
        favButton.setOnClickListener {
            handleFavoriteButtonText(projectId, true)
        }


    }

    /**
     * Function that contacts the database, pushes/removes the project from the favorite list,
     * and changes the button value to the right value
     *
     * @param projectId : id of the project to push
     * @param isClick : differentiate the initialisation of the favorite button from a click on the button
     */
    private fun handleFavoriteButtonText(projectId: String, isClick: Boolean) {
        userdataDatabase.getListOfFavoriteProjects({ projectIds ->
            val isFavorite = projectIds.contains(projectId)
            //we clicked on the favorites button and the project is in the favorite list
            favButton.isEnabled = true
            if (isFavorite && isClick) {
                userdataDatabase.removeFromFavorite(projectId,
                    { onSuccessFavorite(isFavorite) },
                    { showToast(getString(R.string.failure), Toast.LENGTH_SHORT) })
            }
            //clicked on the favorites button and the project isn't in the favorite list
            else if (isClick) {
                userdataDatabase.pushFavoriteProject(projectId,
                    { onSuccessFavorite(isFavorite) },
                    { showToast(getString(R.string.failure), Toast.LENGTH_SHORT) })
            }
            //initialize the button
            else {
                setButtonText(
                    favButton, isFavorite,
                    getString(R.string.favorite_remove_button),
                    getString(R.string.favorite_add_button)
                )
            }


        }, { showToast("failure to contact the database", Toast.LENGTH_SHORT) })
    }

    private fun onSuccessFavorite(isFavorite: Boolean) {
        showToast(getString(R.string.success), Toast.LENGTH_SHORT)
        setButtonText(
            favButton, !isFavorite,
            getString(R.string.favorite_remove_button),
            getString(R.string.favorite_add_button)
        )
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_project_information)


        // get all the text views that will be set
        val title = findViewById<TextView>(R.id.info_project_title)
        val lab = findViewById<TextView>(R.id.info_lab_name)
        descriptionView = findViewById(R.id.info_description)
        val nbOfStudents = findViewById<TextView>(R.id.info_nb_students)
        val type = findViewById<TextView>(R.id.info_available_for)
        val responsible = findViewById<TextView>(R.id.info_responsible_name)
        val creationDate = findViewById<TextView>(R.id.info_creation_date)
        videoView = findViewById(R.id.info_video)
        favButton = findViewById(R.id.favoriteButton)
        favButton.isEnabled = false


        // get the project
        val project: ImmutableProject? = intent.getParcelableExtra(MainActivity.projectString)
        if (project != null) {
            projectVar = project
            // set the text views
            title.text = project.name
            lab.text = project.lab

            responsible.text = project.teacher

            nbOfStudents.text = getString(R.string.display_number_student, project.nbParticipant)
            creationDate.text = SimpleDateFormat(
                getString(R.string.display_creation_date_format),
                Locale.getDefault()
            ).format(project.creationDate)
            type.text =
                if (project.bachelorProject && project.masterProject) getString(R.string.display_bachelor_and_master)
                else if (project.bachelorProject) getString(R.string.display_bachelor_only)
                else if (project.masterProject) getString(R.string.display_master_only)
                else getString(R.string.display_not_found)

            projectDir = File(File(filesDir, "projects"), project.id)

            setupDescriptionWithHTML(project.description)

            videoView.isInvisible = true // hide the videoView before a video is loaded
            if (project.videoUri.isNotEmpty()) {
                val controller = MediaController(this)

                addPauseOnTouchListener(controller)
                setupPlayerListeners(controller)
                handleVideo(project.videoUri)

            }
        } else {
            showToast("An error occurred while loading project.", Toast.LENGTH_LONG)
        }

        // make the back button in the title bar work
        val actionBar = supportActionBar
        actionBar?.setDisplayHomeAsUpEnabled(true)

        setUpApplyButton(findViewById(R.id.applyButton))
        setUpFavoritesButton()
    }

    // pause/start when we touch the video
    @SuppressLint("ClickableViewAccessibility")
    private fun addPauseOnTouchListener(controller: MediaController) {
        videoView.setOnTouchListener { _, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    if (current >= 0) {
                        if (videoView.isPlaying) {
                            videoView.pause()
                        } else {
                            videoView.start()
                        }
                        controller.show() //update controller state
                    }
                }
            }
            true
        }
    }

    private fun setupPlayerListeners(controller: MediaController) {
        videoView.setMediaController(controller)

        // set the controller buttons to play next/prev video
        controller.setPrevNextListeners({
            readNextVideo()
        }, {
            readPrevVideo()
        })

        // play the next video when the video is finished
        videoView.setOnCompletionListener {
            readNextVideo()
        }

        // resize the video view to use full width (with original ratio)
        videoView.setOnPreparedListener { mp ->
            val width = resources.displayMetrics.widthPixels
            val height = (mp.videoHeight / (mp.videoWidth / (width).toFloat())).toInt()
            videoView.layoutParams.height = height
        }
    }


    /**
     * Function that downloads a video either in the proj dir permanent app memory if it's
     * a favorite or one of our own projects. Else, the video is stored in the cache dir which is a
     * memory that is deleted by the phone if it needs it. this function does nothing in the event
     * of on failure being thrown by one of the user or project databases
     * @param videosLinks links of the videos to be downloaded
     */
    private fun handleVideo(videosLinks: List<String>) {
        //get the favorite list, the applied To list and the own projects list
        userdataDatabase.getListOfFavoriteProjects({ favorites ->
            projectDatabase.getAllProjects({ projects ->
                addVideoDownload(videosLinks, favorites.contains(projectVar.id),
                    projects.filter { project -> project.authorId == userId })
            },
                {})

        }, {

        })
    }

    // download all videos and add them to the video player
    private fun addVideoDownload(
        videosLinks: List<String>,
        isFavorite: Boolean,
        ownProjects: List<ImmutableProject>
    ) {

        videosLinks.forEach { link ->
            if (isFavorite || ownProjects.any { project -> project.id == projectVar.id }) {//storing the video in the permanent memory
                movingVideo(link, cacheDir, projectDir)
                storingVideo(link, projectDir)
            } else {
                movingVideo(link, projectDir, cacheDir)//storing
                storingVideo(link, cacheDir)
            }
        }

    }


    private fun storingVideo(link: String, directory: File) {
        fileDB.getFile(link, directory, { file ->
            val uri = Uri.fromFile(file)
            metadataDB.getSubtitlesFromVideo(
                link,
                Locale.ENGLISH.language,
                { subs ->
                    subs?.let {
                        addVideo(uri, it)
                    } ?: run { addVideo(uri, null) }
                }, { addVideo(uri, null) }
            )
        }, { showToast(getString(R.string.could_not_download_video), Toast.LENGTH_LONG) })
    }

    /**
     * moves a video from the delete Directory to the copy Directory
     *
     * @param fileUrl url of the file to be moved
     * @param deleteDirectory directory from which it has to be moved
     * @param copyDirectory directory in which it is moved to
     */
    private fun movingVideo(fileUrl: String, deleteDirectory: File, copyDirectory: File) {
        val fileName = fileDB.getFileName(fileUrl)
        if (fileName == null) {
            showToast("invalid video url name", Toast.LENGTH_LONG)
            return
        }
        val file = File(deleteDirectory, fileName)
        if (file.exists()) {
            val newFile = File(copyDirectory, fileName)
            try {
                Files.move(file, newFile)
            } catch (e: IllegalArgumentException) {
                //do nothing because this exeption is launched when the delete and copy directories
                //are identical (so the video is already where it was asked to be)
            } catch (e: IOException) {
                showToast("failed to move video", Toast.LENGTH_LONG)
            }
        }


    }


    private fun setupDescriptionWithHTML(cleanDescription: String) {
        val imageGetter = ImageGetter2(resources, projectDir)
        val tagHandler = TagHandler2()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            descriptionView.text = Html.fromHtml(
                cleanDescription,
                Html.FROM_HTML_MODE_LEGACY,
                imageGetter,
                tagHandler
            )
        } else {
            descriptionView.text = Html.fromHtml(cleanDescription, imageGetter, tagHandler)
        }

        // Allow user to click on link
        descriptionView.movementMethod = LinkMovementMethod.getInstance()
    }

    private fun showToast(message: String, toastLength: Int) {
        runOnUiThread {
            Toast.makeText(
                this,
                message,
                toastLength
            ).show()
        }
    }


    private inner class ImageGetter2(
        private val res: Resources,
        private val projectDir: File
    ) : Html.ImageGetter {

        override fun getDrawable(source: String): Drawable {
            val holder = BitmapDrawablePlaceHolder(res, null)

            fileDB.getFile(source, projectDir, { file ->
                GlobalScope.launch(Dispatchers.IO) {
                    val draw = Drawable.createFromPath(file.path)
                    if (draw != null) {
                        var width = draw.intrinsicWidth
                        var height = draw.intrinsicHeight

                        val maxWidth = res.displayMetrics.widthPixels
                        if (width > maxWidth) {
                            height = (height / (width / maxWidth.toFloat())).toInt()
                            width = maxWidth
                        }

                        draw.setBounds(0, 0, width, height)
                        holder.setDrawable(draw)
                        holder.setBounds(0, 0, width, height)

                        // force view update
                        withContext(Dispatchers.Main) {
                            descriptionView.text = descriptionView.text
                        }
                    } else {
                        showToast("An error occurred while loading image.", Toast.LENGTH_LONG)
                    }
                }
            }, {
                showToast("An error occurred while downloading image.", Toast.LENGTH_LONG)
            })
            return holder
        }
    }

    private class BitmapDrawablePlaceHolder(res: Resources, bitmap: Bitmap?) : BitmapDrawable(
        res,
        bitmap
    ) {
        private var drawable: Drawable? = null

        override fun draw(canvas: Canvas) {
            drawable?.run { draw(canvas) }
        }

        fun setDrawable(drawable: Drawable) {
            this.drawable = drawable
        }
    }

    // This is used to handle some tags unsupported in version 23 (Android 6.0), such as list ul/ol
    private class TagHandler2 : Html.TagHandler {

        var parent: String? = null
        var index: Int = 1

        override fun handleTag(
            opening: Boolean,
            tag: String,
            output: Editable,
            xmlReader: XMLReader
        ) {
            when (tag) {
                "ul", "ol" -> parent = if (opening) tag else null
                "li" -> {
                    if (opening) {
                        if (parent == "ul")
                            output.append("\n\t•")
                        if (parent == "ol") {
                            output.append("\n\t$index. ")
                            index += 1
                        }
                    }
                }
            }
        }
    }

    private fun createDynamicLink(): Uri {
        val dynamicLink = Firebase.dynamicLinks.dynamicLink {
            link = Uri.parse("https://www.example.com/projectid=" + projectVar.id)
            domainUriPrefix = "https://projmag.page.link/"
            androidParameters {}
        }
        return dynamicLink.uri
    }


    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
        val isOwnProject = (userId == projectVar.authorId)
        menu.findItem(R.id.waitingListButton)?.isVisible = isOwnProject
        menu.findItem(R.id.editButton)?.isVisible = isOwnProject
        return super.onPrepareOptionsMenu(menu)
    }

    /**
     * Creates menu with a share button and a QR code button.
     */
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_project_information, menu)
        return true
    }

    /**
     * QR code button opens QRCodeActivity and share button generates
     * a deep link and opens the android share activity.
     */
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == R.id.shareButton) {
            val linkToSend = createDynamicLink()
            val sendIntent = Intent(Intent.ACTION_SEND)
            sendIntent.putExtra(Intent.EXTRA_TEXT, linkToSend.toString())
            sendIntent.type = "text/plain"
            startActivity(sendIntent)
            return true
        } else if (item.itemId == R.id.waitingListButton) {
            if (userId == projectVar.authorId) {
                val intent = Intent(this, WaitingListActivity::class.java)
                intent.putExtra(MainActivity.projectIdString, projectVar.id)
                startActivity(intent)
            } else {
                //this should not happen, unless the user was disconnected after loading the project view
                showToast(resources.getString(R.string.waiting_not_allowed), Toast.LENGTH_LONG)
            }
        } else if (item.itemId == R.id.generateQRCodeButton) {
            val linkToSend = createDynamicLink()

            val qrImage = QRCode.from(linkToSend.toString()).withSize(800, 800)
            val stream = ByteArrayOutputStream()
            qrImage.bitmap().compress(Bitmap.CompressFormat.PNG, 100, stream)
            val byteArray: ByteArray = stream.toByteArray()

            val intent = Intent(this, QRCodeActivity::class.java)
            intent.putExtra("qrcode", byteArray)
            startActivity(intent)
            return true
        } else if (item.itemId == R.id.editButton) {
            val intent = Intent(this, ProjectCreationActivity::class.java)
            intent.putExtra(EDIT_EXTRA, projectVar)
            startActivity(intent)
            finish()
        }
        return super.onOptionsItemSelected(item)
    }

}