Browse Source

Merge remote-tracking branch 'origin/master'

cooperku_kdanmobile 6 years ago
parent
commit
6e0d5e1b3b

+ 26 - 0
src/androidTest/java/com/bomostory/sceneeditmodule/CoverDrawerTest.kt

@@ -0,0 +1,26 @@
+package com.bomostory.sceneeditmodule
+
+import android.support.test.rule.GrantPermissionRule
+import org.junit.Test
+
+import org.junit.Assert.*
+import org.junit.Rule
+import java.io.File
+
+class CoverDrawerTest {
+
+    @get:Rule
+    var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+
+    @Test
+    fun drawFrontCover() {
+
+        val w = 360 * 10
+        val h = 360 * 10
+        val title = "This is fucking good story"
+        val author = "Wayne"
+        val coverFile = File("")
+        val bitmap = CoverDrawer.drawFrontCover(w, h, title, author, coverFile)
+        print("")
+    }
+}

+ 1 - 0
src/main/java/com/bomostory/sceneeditmodule/Config.kt

@@ -9,6 +9,7 @@ object Config {
     private const val PROJECTS_FOLDER_PATH = "/Bomo/Projects"
     const val RECORD_FOLDER_NAME = "Record"
     const val IMAGE_FOLDER_NAME = "Image"
+    const val MUSIC_FOLDER_NAME = "Music"
     val ASSETS_FOLDER = File(Environment.getExternalStorageDirectory(), ASSETS_FOLDER_PATH)
     val PROJECTS_FOLDER = File(Environment.getExternalStorageDirectory(), PROJECTS_FOLDER_PATH)
     const val PROJECT_FILE_NAME = "index"

+ 56 - 14
src/main/java/com/bomostory/sceneeditmodule/CoverDrawer.kt

@@ -2,30 +2,72 @@ package com.bomostory.sceneeditmodule
 
 import android.content.Context
 import android.graphics.*
+import android.text.TextPaint
+import android.text.TextUtils
 import com.example.tfat.myapplication.R
+import java.io.File
 
 object CoverDrawer {
-    private const val DEFAULT_HEIGHT = 1080f
 
-    fun drawFrontCover(width: Int, height: Int, name: String, coverColor: Int): Bitmap {
-        val scale = height.toFloat() / DEFAULT_HEIGHT
-        val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
-            color = Color.WHITE
-            textSize = 100f * scale
+    fun drawFrontCover(width: Int, height: Int, name: String, author: String, coverFile: File): Bitmap {
+        return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444).apply {
+            Canvas(this).apply {
+                if (coverFile.exists()) {
+                    val cover = BitmapFactory.decodeFile(coverFile.absolutePath)
+                    val src = Rect(0, 0, cover.width, cover.height)
+                    val dst = Rect(0, 0, width, height)
+                    drawBitmap(cover, src, dst, null)
+                }
+                drawMastTileAuthor(name, author)
+            }
+        }
+    }
+
+    private fun Canvas.drawMastTileAuthor(title: String, author: String) {
+        val maskMarginTop = height * (232f / 360f)
+        val maskMarginBottom = height * (344f / 360f)
+        val titleMarginTop = height * ((232f + 16f) / 360f)
+        val titleHeight = height * (47f / 360f)
+        val titleSize = height * (40f / 360f)
+        val authorHeight = height * (24f / 360f)
+        val authorMarginTop = height * ((232f + 16f + 47f + 9f) / 360f)
+        val authorSize = height * (20f / 360f)
+
+        val maskRect = RectF(0f, maskMarginTop, width.toFloat(), maskMarginBottom)
+        val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+            color = Color.parseColor("#66ffffff")
+            style = Paint.Style.FILL_AND_STROKE
+        }
+        drawRect(maskRect, maskPaint)
+
+        val titlePaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
+            color = Color.BLACK
+            textSize = titleSize
+            textAlign = Paint.Align.CENTER
+            flags += Paint.FAKE_BOLD_TEXT_FLAG
+        }
+        val titleText = TextUtils.ellipsize(title, titlePaint, width.toFloat(), TextUtils.TruncateAt.END)
+        val titleX = width / 2f
+        val titleY = titleMarginTop + titleHeight
+        drawText(titleText, 0, titleText.length, titleX, titleY, titlePaint)
+
+        val authorPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
+            color = Color.BLACK
+            textSize = authorSize
             textAlign = Paint.Align.CENTER
             flags += Paint.FAKE_BOLD_TEXT_FLAG
         }
+        val authorText = TextUtils.ellipsize(author, authorPaint, width.toFloat(), TextUtils.TruncateAt.END)
+        val authorX = width / 2f
+        val authorY = authorMarginTop + authorHeight
+        drawText(authorText, 0, authorText.length, authorX, authorY, authorPaint)
+    }
+
+    fun drawFrontCover(width: Int, height: Int, name: String, author: String, coverColor: Int): Bitmap {
         return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444).apply {
             Canvas(this).apply {
                 drawColor(coverColor)
-                val marginBottom = height / 20
-                val rect = Rect(0, height - height / 3 - marginBottom, width, height - marginBottom)
-                val p = Paint(Paint.ANTI_ALIAS_FLAG).apply {
-                    color = Color.parseColor("#66ffffff")
-                    style = Paint.Style.FILL_AND_STROKE
-                }
-                drawRect(rect, p)
-                drawText(name, rect.centerX().toFloat(), rect.centerY().toFloat(), paint)
+                drawMastTileAuthor(name, author)
             }
         }
     }

+ 16 - 5
src/main/java/com/bomostory/sceneeditmodule/PdfMaker.kt

@@ -15,9 +15,15 @@ object PdfMaker {
             val width = 1920
             val height = 1080
             val coverWidth = width / 2
+            val coverFile = project.coverFile
             emitter.onNext(progress++ / total)
-            val front = CoverDrawer.drawFrontCover(coverWidth, height, project.name
-                    ?: "", project.frontCoverColor.getColor(context))
+            val name = project.name ?: ""
+            val author = project.author ?: ""
+            val front = if (coverFile == null) {
+                CoverDrawer.drawFrontCover(width, height, name, author, project.frontCoverColor.getColor(context))
+            } else {
+                CoverDrawer.drawFrontCover(width, height, name, author, coverFile)
+            }
             emitter.onNext(progress++ / total)
             val back = CoverDrawer.drawBackCover(context, coverWidth, height, project.backCoverColor.getColor(context))
             emitter.onNext(progress++ / total)
@@ -38,11 +44,16 @@ object PdfMaker {
             var progress = 0
             val width = 1920 / 2
             val height = 1080
-            val name = project?.name ?: ""
-            val author = project?.author ?: ""
+            val name = project.name ?: ""
+            val author = project.author ?: ""
+            val coverFile = project.coverFile
             emitter.onNext(progress++ / total)
             /** front **/
-            val front = CoverDrawer.drawFrontCover(width, height, name, project.frontCoverColor.getColor(context))
+            val front = if (coverFile == null) {
+                CoverDrawer.drawFrontCover(width, height, name, author, project.frontCoverColor.getColor(context))
+            } else {
+                CoverDrawer.drawFrontCover(width, height, name, author, coverFile)
+            }
             emitter.onNext(progress++ / total)
             /** back **/
             val back = CoverDrawer.drawBackCover(context, width, height, project.backCoverColor.getColor(context))

+ 7 - 7
src/main/java/com/bomostory/sceneeditmodule/SceneEditActivity.kt

@@ -1079,7 +1079,7 @@ class SceneEditActivity : AppCompatActivity(), ActorAdapter.OnActorDragListener,
                 it.scenes?.let {
                     it[currentSceneIndex].layers[currentLayerIndex].actors.removeAt(actor.positionZ)
                     it[currentSceneIndex].layers[currentLayerIndex].actors.add(actor)
-                    setActorPositionZ(it[currentLayerIndex].layers[currentLayerIndex].actors)
+                    setActorPositionZ(it[currentSceneIndex].layers[currentLayerIndex].actors)
                     sceneEditView.scene = it[currentSceneIndex]
                     popupWindow.dismiss()
                 }
@@ -1091,7 +1091,7 @@ class SceneEditActivity : AppCompatActivity(), ActorAdapter.OnActorDragListener,
                 it.scenes?.let {
                     it[currentSceneIndex].layers[currentLayerIndex].actors.remove(actor)
                     it[currentSceneIndex].layers[currentLayerIndex].actors.add(0, actor)
-                    setActorPositionZ(it[currentLayerIndex].layers[currentLayerIndex].actors)
+                    setActorPositionZ(it[currentSceneIndex].layers[currentLayerIndex].actors)
                     sceneEditView.scene = it[currentSceneIndex]
                     popupWindow.dismiss()
                 }
@@ -1102,7 +1102,7 @@ class SceneEditActivity : AppCompatActivity(), ActorAdapter.OnActorDragListener,
             project.story?.let {
                 it.scenes?.let {
                     it[currentSceneIndex].layers[currentLayerIndex].actors.removeAt(actor.positionZ)
-                    setActorPositionZ(it[currentLayerIndex].layers[currentLayerIndex].actors)
+                    setActorPositionZ(it[currentSceneIndex].layers[currentLayerIndex].actors)
                     sceneEditView.scene = it[currentSceneIndex]
                     popupWindow.dismiss()
                 }
@@ -1261,7 +1261,7 @@ class SceneEditActivity : AppCompatActivity(), ActorAdapter.OnActorDragListener,
                 it.scenes?.let {
                     it[currentSceneIndex].layers[currentLayerIndex].actors.remove(actor)
                     it[currentSceneIndex].layers[currentLayerIndex].actors.add(actor)
-                    setActorPositionZ(it[currentLayerIndex].layers[currentLayerIndex].actors)
+                    setActorPositionZ(it[currentSceneIndex].layers[currentLayerIndex].actors)
                     sceneEditView.scene = it[currentSceneIndex]
                     popupWindow.dismiss()
                 }
@@ -1273,7 +1273,7 @@ class SceneEditActivity : AppCompatActivity(), ActorAdapter.OnActorDragListener,
                 it.scenes?.let {
                     it[currentSceneIndex].layers[currentLayerIndex].actors.remove(actor)
                     it[currentSceneIndex].layers[currentLayerIndex].actors.add(0, actor)
-                    setActorPositionZ(it[currentLayerIndex].layers[currentLayerIndex].actors)
+                    setActorPositionZ(it[currentSceneIndex].layers[currentLayerIndex].actors)
                     sceneEditView.scene = it[currentSceneIndex]
                     popupWindow.dismiss()
                 }
@@ -1284,7 +1284,7 @@ class SceneEditActivity : AppCompatActivity(), ActorAdapter.OnActorDragListener,
             project.story?.let {
                 it.scenes?.let {
                     it[currentSceneIndex].layers[currentLayerIndex].actors.removeAt(actor.positionZ)
-                    setActorPositionZ(it[currentLayerIndex].layers[currentLayerIndex].actors)
+                    setActorPositionZ(it[currentSceneIndex].layers[currentLayerIndex].actors)
                     sceneEditView.scene = it[currentSceneIndex]
                     popupWindow.dismiss()
                 }
@@ -1451,7 +1451,7 @@ class SceneEditActivity : AppCompatActivity(), ActorAdapter.OnActorDragListener,
                 it.scenes?.let {
                     it[currentSceneIndex].layers[selectLayer].actors.remove(actor)
                     it[currentSceneIndex].layers[selectLayer].actors.add(actor)
-                    setActorPositionZ(it[selectLayer].layers[selectLayer].actors)
+                    setActorPositionZ(it[currentSceneIndex].layers[selectLayer].actors)
                     sceneEditView.scene = it[currentSceneIndex]
                     popupWindow.dismiss()
                     layerManagementDialog.setData(it[currentSceneIndex])

+ 48 - 39
src/main/java/com/bomostory/sceneeditmodule/SuperMovieMaker.kt

@@ -2,13 +2,13 @@ package com.bomostory.sceneeditmodule
 
 import android.content.Context
 import android.media.MediaMetadataRetriever
-import android.os.Environment
 import android.util.Log
 import com.bomostory.sceneeditmodule.screen.movie.MovieEditActivity.Companion.FPS
 import com.bomostory.sceneeditmodule.basicdata.Music
 import com.bomostory.sceneeditmodule.basicdata.Project
-import com.bomostory.sceneeditmodule.basicdata.Story
+import com.bomostory.sceneeditmodule.utils.FileUtils
 import com.example.exportmedia.MediaHelper
+import com.example.exportmedia.audio.AudioConcat
 import com.example.exportmedia.audio.AudioLooper
 import com.example.exportmedia.data.AudioSource
 import com.example.exportmedia.vedio.MovieMaker
@@ -35,9 +35,9 @@ class SuperMovieMaker {
             try {
                 val audioSources = ArrayList<AudioSource>()
                 project.story?.let {
-                    audioSources.addAll(generateRecordSource(it))
+                    audioSources.add(generateRecordSource(project, mediaHelper))
                 }
-                audioSources.addAll(generateAudioSource(musics, mediaHelper))
+                audioSources.addAll(generateAudioSource(project, musics, mediaHelper))
 
                 val movieBuilder = MovieMaker.Builder(mediaHelper)
                 movieBuilder.fps = FPS
@@ -78,59 +78,68 @@ class SuperMovieMaker {
                 emitter.onNext("Encoding video")
                 movieBuilder.build().output(outputFile).blockingSubscribe()
             } catch (e: Exception) {
-
+                e.printStackTrace()
             }
             emitter.onComplete()
         }
     }
 
-    private fun generateRecordSource(story: Story): ArrayList<AudioSource> {
-        val audioSources = ArrayList<AudioSource>()
-
-        var period: Long = 0
-        for (scene in story.scenes) {
-            val mediaMetadataRetriever = MediaMetadataRetriever()
-            val audioSource = AudioSource()
-
-            scene.recordPath?.let {
-                try {
-                    mediaMetadataRetriever.setDataSource(it)
-
-                    audioSource.path = scene.recordPath
-                    audioSource.duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
-                    audioSource.offsetSec = period.toFloat() / 1000
+    private fun generateRecordSource(project: Project, mediaHelper: MediaHelper): AudioSource {
+        val audioSource = AudioSource()
 
-                    audioSources.add(audioSource)
-                } catch (e: Exception) {
+        val recordPath = "${FileUtils.getProjectPath(project)}/${Config.RECORD_FOLDER_NAME}"
+        val fileName = "${System.currentTimeMillis()}.mp3"
+        val outputFile = File(recordPath, fileName)
 
+        val audioConcatBuilder = AudioConcat.Builder(mediaHelper)
+        project.story?.let {
+            for (scene in it.scenes) {
+                scene.recordPath?.let {
+                    audioConcatBuilder.addInputSource(File(it))
                 }
             }
-
-            period += scene.record?.period ?: 0
         }
-        return audioSources
+
+        val audioConcat = audioConcatBuilder.build()
+            audioConcat.output(outputFile).blockingFirst()
+
+        val mediaMetadataRetriever = MediaMetadataRetriever()
+        mediaMetadataRetriever.setDataSource(outputFile.path)
+
+        audioSource.path = outputFile.path
+        audioSource.duration =  mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
+        return audioSource
     }
 
-    private fun generateAudioSource(musics: List<Music>, mediaHelper: MediaHelper): ArrayList<AudioSource> {
+    private fun generateAudioSource(project: Project, musics: List<Music>, mediaHelper: MediaHelper): ArrayList<AudioSource> {
         val audioSources = ArrayList<AudioSource>()
+
+        val musicPath = "${FileUtils.getProjectPath(project)}/${Config.MUSIC_FOLDER_NAME}"
         for (music in musics) {
-            val fileName = "test${System.currentTimeMillis()}.mp3"
-            val outputFile = File(Environment.getExternalStorageDirectory(), fileName)
+            val audioSource = AudioSource()
 
-            val audioLooperBuilder = AudioLooper.Builder(mediaHelper)
-            audioLooperBuilder.loopAudioFile = File(music.path)
-            audioLooperBuilder.loopDuration = music.loopDuration.toInt()
+            if (music.isLoop) {
+                val fileName = "${System.currentTimeMillis()}.mp3"
+                val outputFile = File(musicPath, fileName)
 
-            val audioLooper = audioLooperBuilder.build()
-            audioLooper.output(outputFile).blockingFirst()
+                val audioLooperBuilder = AudioLooper.Builder(mediaHelper)
+                audioLooperBuilder.loopAudioFile = File(music.path)
+                audioLooperBuilder.loopDuration = music.loopDuration.toInt()
 
-            val mediaMetadataRetriever = MediaMetadataRetriever()
-            mediaMetadataRetriever.setDataSource(outputFile.path)
+                val audioLooper = audioLooperBuilder.build()
+                audioLooper.output(outputFile).blockingFirst()
 
-            val audioSource = AudioSource()
-            audioSource.path = outputFile.path
-            audioSource.duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
-            audioSource.offsetSec = music.offsetTime.toFloat() / 1000
+                val mediaMetadataRetriever = MediaMetadataRetriever()
+                mediaMetadataRetriever.setDataSource(outputFile.path)
+
+                audioSource.path = outputFile.path
+                audioSource.duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
+                audioSource.offsetSec = music.offsetTime.toFloat() / 1000
+            } else {
+                audioSource.path = music.path
+                audioSource.duration = music.duration
+                audioSource.offsetSec = music.offsetTime.toFloat() / 1000
+            }
             audioSources.add(audioSource)
         }
         return audioSources

+ 61 - 78
src/main/java/com/bomostory/sceneeditmodule/screen/movie/MovieEditActivity.kt

@@ -1,13 +1,11 @@
 package com.bomostory.sceneeditmodule.screen.movie
 
-import android.app.AlertDialog
 import android.app.ProgressDialog
 import android.arch.lifecycle.Observer
 import android.arch.lifecycle.ViewModelProviders
 import android.content.DialogInterface
 import android.content.Intent
 import android.os.Bundle
-import android.os.Environment
 import android.os.Handler
 import android.support.v4.app.DialogFragment
 import android.support.v4.content.FileProvider
@@ -33,7 +31,6 @@ import com.bomostory.sceneeditmodule.share.ExportPdfDialog
 import com.bomostory.sceneeditmodule.share.ShareDialog
 import com.bomostory.sceneeditmodule.utils.FileUtils
 import com.bomostory.sceneeditmodule.utils.MoviePlayerV2
-import com.bomostory.sceneeditmodule.utils.MusicPlayerV2
 import com.bomostory.sceneeditmodule.utils.Utils
 import com.example.exportmedia.MediaHelper
 import com.example.tfat.myapplication.R
@@ -52,93 +49,47 @@ class MovieEditActivity : AppCompatActivity(),
         MusicEditDialog.OnFragmentInteractionListener {
 
     override fun onMusicImportClick(music: Music) {
+        viewModel.importMusic(music)
         moviePlayer.init()
 
-        val musics: ArrayList<Music> = viewModel.musicListLiveData.value as ArrayList<Music>
-        musics.add(music)
-        viewModel.musicListLiveData.value = musics
-        viewModel.musicLiveData.value = music
-
-        val dialogFragment = supportFragmentManager.findFragmentByTag(MUSIC_SELECT_DIALOG_TAG) as DialogFragment
-        dialogFragment.dismiss()
+        musicSelectDialog?.dismiss()
     }
 
     override fun onMusicPreCastClick(music: Music) {
-        musicPlayer.apply {
-            this.music = music
-            stop()
-            init()
-            start()
-        }
+        viewModel.precastMusic(music)
     }
 
     override fun onMusicPlayClick(music: Music, startPosition: Int) {
-        musicPlayer.apply {
-            this.music = music
-            playChangedListener = object : MusicPlayerV2.OnPlayChangedListener {
-                override fun OnPlayChanged(position: Int) {
-                    runOnUiThread {
-                        musicEditDialog?.currentPosition = position
-                    }
-                }
-            }
-            init()
-            seekTo(startPosition)
-            start()
-        }
+        viewModel.playMusic(music, startPosition)
     }
 
     override fun onMusicVolumeChanged(volume: Float) {
-        musicPlayer.setVolume(volume)
+        viewModel.setMusicVolume(volume)
     }
 
     override fun onMusicPauseClick() {
-        musicPlayer.pause()
+        viewModel.pauseMusic()
     }
 
     override fun onMusicStop() {
-        musicPlayer.stop()
+        viewModel.stopMusic()
     }
 
     override fun onMusicEditFinish(music: Music) {
+        viewModel.importEditMusic(music)
         moviePlayer.init()
-
-        //TODO change path to correct path
-        val outputFile = File(Environment.getExternalStorageDirectory(), "test${System.currentTimeMillis()}.mp3")
-
-        val pd = ProgressDialog(this).apply {
-            setMessage("progressing")
-            setCancelable(false)
-        }
-
-        viewModel.exportEditAudio(music, outputFile)
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .doOnSubscribe {
-                    pd.show()
-                }
-                .doFinally {
-                    pd.dismiss()
-                }
-                .subscribe({
-                    music.path = outputFile.path
-
-                    val musics: ArrayList<Music> = viewModel.musicListLiveData.value as ArrayList<Music>
-                    musics[musics.indexOf(viewModel.musicLiveData.value)] = music
-                    viewModel.musicListLiveData.value = musics
-                    viewModel.musicLiveData.value = music
-                }, {
-
-                })
     }
 
     override fun onDismiss(dialog: DialogInterface?) {
-        musicPlayer.stop()
+        viewModel.stopMusic()
         delayedHide(100)
     }
 
     private var musicEditDialog: MusicEditDialog? = null
+    private var musicSelectDialog: MusicSelectDialog? = null
+
     private val shareDialog = ShareDialog()
+    private lateinit var progressDialog: ProgressDialog
 
     private val mHideHandler = Handler()
     private val mHidePart2Runnable = Runnable {
@@ -159,8 +110,6 @@ class MovieEditActivity : AppCompatActivity(),
 
     private val moviePlayer = MoviePlayerV2()
 
-    private val musicPlayer = MusicPlayerV2()
-
     private val mediaHelper = MediaHelper()
 
     companion object {
@@ -177,6 +126,11 @@ class MovieEditActivity : AppCompatActivity(),
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_movie_edit)
 
+        progressDialog = ProgressDialog(this).apply {
+            setMessage("progressing")
+            setCancelable(false)
+        }
+
         mediaHelper.init(this)
 
         viewModel = ViewModelProviders.of(this).get(MovieEditViewModel::class.java)
@@ -205,6 +159,20 @@ class MovieEditActivity : AppCompatActivity(),
                 movieEditView.currentAudioPosition = indexOf(it)
             }
         })
+        viewModel.musicPlayPosition.observe(this, Observer {
+            it?.let {
+                musicEditDialog?.currentPosition = it
+            }
+        })
+        viewModel.showProgressingLiveData.observe(this, Observer {
+            it?.let {
+                if (it) {
+                    if (!progressDialog.isShowing) progressDialog.show()
+                } else {
+                    if (progressDialog.isShowing) progressDialog.dismiss()
+                }
+            }
+        })
 
         supportActionBar?.setDisplayHomeAsUpEnabled(true)
 
@@ -252,11 +220,7 @@ class MovieEditActivity : AppCompatActivity(),
             }
         }
         movieEditView.onLoopSwitchChangedListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
-            val musics: ArrayList<Music> = viewModel.musicListLiveData.value as ArrayList<Music>
-            for (music in musics) {
-                music.isLoop = isChecked
-            }
-            viewModel.musicListLiveData.value = musics
+            viewModel.setMusicLoopEnable(isChecked)
         }
 
         moviePlayer.moviePlayListener = object : MoviePlayerV2.OnMoviePlayListener {
@@ -318,8 +282,8 @@ class MovieEditActivity : AppCompatActivity(),
     private fun addMusic(view: View) {
         viewModel.parseDeviceMusic(contentResolver)
 
-        val musicSelectDialog = MusicSelectDialog.newInstance(Gson().toJson(viewModel.musicSource))
-        musicSelectDialog.show(supportFragmentManager, MUSIC_SELECT_DIALOG_TAG)
+        musicSelectDialog = MusicSelectDialog.newInstance(Gson().toJson(viewModel.musicSource))
+        musicSelectDialog?.show(supportFragmentManager, MUSIC_SELECT_DIALOG_TAG)
     }
 
     private fun editMusic(view: View) {
@@ -328,9 +292,7 @@ class MovieEditActivity : AppCompatActivity(),
     }
 
     private fun deleteMusic(view: View) {
-        val musics: ArrayList<Music> = viewModel.musicListLiveData.value as ArrayList<Music>
-        musics.remove(viewModel.musicLiveData.value)
-        viewModel.musicListLiveData.value = musics
+        viewModel.deleteMusic()
     }
 
     private fun onClickSaveAndShareBtn(view: View) {
@@ -393,11 +355,11 @@ class MovieEditActivity : AppCompatActivity(),
                 onSave = View.OnClickListener { it ->
                     if (storyName == "") {
                         showEmptyProjectNameMsg()
-                    /** TODO: 目前不支援 rename **/
-                    /**
-                    } else if (storyName != oldName && viewModel.isProjectNameExist(storyName)) {
+                        /** TODO: 目前不支援 rename **/
+                        /**
+                        } else if (storyName != oldName && viewModel.isProjectNameExist(storyName)) {
                         showDuplicatedProjectNameMsg()
-                    **/
+                         **/
                     } else {
                         if (coverFile == null) {
                             project.coverFile = null
@@ -416,7 +378,7 @@ class MovieEditActivity : AppCompatActivity(),
                                 .doOnSubscribe { pd.show() }
                                 .doFinally { pd.dismiss() }
                                 .subscribe {
-//                                    projectManager.reloadProjects()
+                                    //                                    projectManager.reloadProjects()
                                     dismiss()
                                 }
                     }
@@ -610,4 +572,25 @@ class MovieEditActivity : AppCompatActivity(),
                     onComplete.run()
                 })
     }
+
+    override fun onPause() {
+        super.onPause()
+
+        musicSelectDialog?.dismiss()
+        musicEditDialog?.dismiss()
+
+        moviePlayer.pause()
+    }
+
+    override fun onStop() {
+        super.onStop()
+
+        moviePlayer.init()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+
+        viewModel.onDestroy()
+    }
 }

+ 157 - 31
src/main/java/com/bomostory/sceneeditmodule/screen/movie/MovieEditViewModel.kt

@@ -5,8 +5,10 @@ import android.arch.lifecycle.ViewModel
 import android.content.ContentResolver
 import android.content.Context
 import android.media.MediaMetadataRetriever
+import com.bomostory.sceneeditmodule.Config
 import com.bomostory.sceneeditmodule.basicdata.*
 import com.bomostory.sceneeditmodule.utils.FileUtils
+import com.bomostory.sceneeditmodule.utils.MusicPlayerV2
 import com.bomostory.sceneeditmodule.utils.TimeUtils
 import com.example.bomocloud.BoMoCloud
 import com.example.bomocloud.video.UploadVideoData
@@ -37,6 +39,10 @@ class MovieEditViewModel : ViewModel() {
 
     val musicLiveData = MutableLiveData<Music>()
 
+    val musicPlayPosition = MutableLiveData<Int>()
+
+    val showProgressingLiveData = MutableLiveData<Boolean>()
+
     val musicListLiveData = MutableLiveData<List<Music>>().apply {
         value = ArrayList()
     }
@@ -59,6 +65,8 @@ class MovieEditViewModel : ViewModel() {
 
     var musicSource = MusicSource()
 
+    private val musicPlayer = MusicPlayerV2()
+
 
     fun parseDeviceMusic(contentResolver: ContentResolver) {
         val libMusicPaths = ArrayList<String?>()
@@ -69,19 +77,134 @@ class MovieEditViewModel : ViewModel() {
         musicSource.device = DataParser.parseMusic(contentResolver, libMusicPaths)
     }
 
-    fun exportEditAudio(music: Music, outputFile: File): Observable<String> {
-        if (music.endTime - music.startTime > storyPeriodLiveData.value ?: 0) {
-            music.endTime = music.startTime + (storyPeriodLiveData.value ?: 0)
+    fun importMusic(music: Music) {
+        val musics: ArrayList<Music> = musicListLiveData.value as ArrayList<Music>
+        project?.let {
+            val musicFile = File(music.path)
+            val outputFile = createMusicFile()
+
+            outputFile?.let {
+                musicFile.copyTo(it)
+                music.path = it.path
+            }
+        }
+        musics.add(music)
+        musicListLiveData.value = musics
+        musicLiveData.value = music
+    }
+
+    fun importEditMusic(music: Music) {
+        val musics: ArrayList<Music> = musicListLiveData.value as ArrayList<Music>
+        val outputFile = createMusicFile()
+        outputFile?.let {
+            if (music.endTime - music.startTime > storyPeriodLiveData.value ?: 0) {
+                music.endTime = music.startTime + (storyPeriodLiveData.value ?: 0)
+            }
+
+            val audioEditorBuilder = AudioEditor.Builder(mediaHelper)
+            audioEditorBuilder.editAudioFile = File(music.path)
+            audioEditorBuilder.startTime = TimeUtils.getEncodeTimeFormt(music.startTime)
+            audioEditorBuilder.endTime = TimeUtils.getEncodeTimeFormt(music.endTime)
+            audioEditorBuilder.volume = music.volume
+
+            val audioEditor = audioEditorBuilder.build()
+            audioEditor.output(it)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .doOnSubscribe {
+                        showProgressingLiveData.value = true
+                    }
+                    .doFinally {
+                        showProgressingLiveData.value = false
+                    }.subscribe({
+                        music.path = outputFile.path
+                        musics[musics.indexOf(musicLiveData.value)] = music
+                        musicListLiveData.value = musics
+                        musicLiveData.value = music
+                    }, {
+
+                    })
+        }
+    }
+
+    fun setMusicLoopEnable(enable: Boolean) {
+        val musics: ArrayList<Music> = musicListLiveData.value as ArrayList<Music>
+        musicLiveData.value?.let {
+            it.isLoop = enable
+            musics[musics.indexOf(it)] = it
+        }
+        musicListLiveData.value = musics
+    }
+
+    fun deleteMusic() {
+        val musics: ArrayList<Music> = musicListLiveData.value as ArrayList<Music>
+        musics.remove(musicLiveData.value)
+        musicListLiveData.value = musics
+        if (musics.size > 0) {
+            musicLiveData.value = musics[0]
+        }
+    }
+
+    fun precastMusic(music: Music) {
+        musicPlayer.apply {
+            this.music = music
+            stop()
+            init()
+            start()
         }
+    }
+
+    fun playMusic(music: Music, startPosition: Int) {
+        musicPlayer.apply {
+            this.music = music
+            playChangedListener = object : MusicPlayerV2.OnPlayChangedListener {
+                override fun onPlayChanged(position: Int) {
+                    musicPlayPosition.postValue(position)
+                }
+            }
+            init()
+            seekTo(startPosition)
+            start()
+        }
+    }
 
-        val audioEditorBuilder = AudioEditor.Builder(mediaHelper)
-        audioEditorBuilder.editAudioFile = File(music.path)
-        audioEditorBuilder.startTime = TimeUtils.getEncodeTimeFormt(music.startTime)
-        audioEditorBuilder.endTime = TimeUtils.getEncodeTimeFormt(music.endTime)
-        audioEditorBuilder.volume = music.volume
+    fun pauseMusic(){
+        musicPlayer.pause()
+    }
+
+    fun stopMusic(){
+        musicPlayer.stop()
+    }
+
+    fun setMusicVolume(volume: Float) {
+        musicPlayer.setVolume(volume)
+    }
+
+    fun uploadVideo(context: Context, file: File, project: Project): Observable<UploadVideoData> {
+        val id = context.getString(R.string.bomo_cloud_client_id)
+        val secret = context.getString(R.string.bomo_cloud_client_secret)
+        val author = project.author ?: ""
+        val name = project.name ?: ""
+        val category = project.category.id
+        val filePart = MultipartBody.Part.createFormData("file", file.name, RequestBody.create(MediaType.parse("video/*"), file))
+        return BoMoCloud(id, secret).videoService
+                .uploadVideo(author, name, category, filePart)
+    }
 
-        val audioEditor = audioEditorBuilder.build()
-        return audioEditor.output(outputFile)
+    fun saveProject(context: Context, project: Project, oldName: String): Completable {
+        var newProject = project
+        project.name?.let { projectName ->
+            if (projectName != oldName) {
+                newProject = com.bomostory.sceneeditmodule.utils.FileUtils.changeProjectName(oldName, projectName)
+            }
+        }
+        return FileUtils.saveProject(context, newProject, 1920, 1080)
+                .andThen {
+                    onProjectSavedListeners.forEach { it.run() }
+                    it.onComplete()
+                }
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
     }
 
     private fun parseThemeMusic(project: Project): ArrayList<Music> {
@@ -114,31 +237,34 @@ class MovieEditViewModel : ViewModel() {
         return totalPeriod
     }
 
-    fun uploadVideo(context: Context, file: File, project: Project): Observable<UploadVideoData> {
-        val id = context.getString(R.string.bomo_cloud_client_id)
-        val secret = context.getString(R.string.bomo_cloud_client_secret)
-        val author = project.author ?: ""
-        val name = project.name ?: ""
-        val category = project.category.id
-        val filePart = MultipartBody.Part.createFormData("file", file.name, RequestBody.create(MediaType.parse("video/*"), file))
-        return BoMoCloud(id, secret).videoService
-                .uploadVideo(author, name, category, filePart)
-    }
+    private fun createMusicFile(): File? {
+        project?.let {
+            val musicPath = "${FileUtils.getProjectPath(it)}/${Config.MUSIC_FOLDER_NAME}"
+            val fileName = "${System.currentTimeMillis()}.mp3"
+            val outputFile = File(musicPath, fileName)
 
-    fun saveProject(context: Context, project: Project, oldName: String): Completable {
-        var newProject = project
-        project.name?.let { projectName ->
-            if (projectName != oldName) {
-                newProject = com.bomostory.sceneeditmodule.utils.FileUtils.changeProjectName(oldName, projectName)
+            val parentFile = outputFile.parentFile
+            if (!parentFile.exists() || !parentFile.isDirectory) {
+                parentFile.mkdirs()
             }
+
+            return outputFile
         }
-        return FileUtils.saveProject(context, newProject, 1920, 1080)
-                .andThen {
-                    onProjectSavedListeners.forEach { it.run() }
-                    it.onComplete()
+        return null
+    }
+
+    fun onDestroy() {
+        musicPlayer.release()
+
+        project?.let {
+            val musicPath = "${FileUtils.getProjectPath(it)}/${Config.MUSIC_FOLDER_NAME}"
+            val musicFolder = File(musicPath)
+            if (musicFolder.exists()) {
+                for (file in musicFolder.listFiles()) {
+                    file.delete()
                 }
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
+            }
+        }
     }
 
 }

+ 8 - 0
src/main/java/com/bomostory/sceneeditmodule/screen/view/DialogueView.kt

@@ -158,6 +158,8 @@ class DialogueView : EditActorView{
                         isDialogue = dialogue.isDialogue
                         isMirror = dialogue.isMirror
                         opacity = actor.opacity
+                        dialogColor = actor.dialogColor
+                        dialogType = actor.dialogType
                     }
                     dialogue = dialogueData
                 }
@@ -225,6 +227,8 @@ class DialogueView : EditActorView{
                         isDialogue = dialogue.isDialogue
                         isMirror = dialogue.isMirror
                         opacity = actor.opacity
+                        dialogColor = actor.dialogColor
+                        dialogType = actor.dialogType
                     }
                     dialogue = dialogueData
                 }
@@ -290,6 +294,8 @@ class DialogueView : EditActorView{
                         isDialogue = dialogue.isDialogue
                         isMirror = dialogue.isMirror
                         opacity = actor.opacity
+                        dialogColor = actor.dialogColor
+                        dialogType = actor.dialogType
                     }
                     dialogue = dialogueData
                 }
@@ -355,6 +361,8 @@ class DialogueView : EditActorView{
                         isDialogue = dialogue.isDialogue
                         isMirror = dialogue.isMirror
                         opacity = actor.opacity
+                        dialogColor = actor.dialogColor
+                        dialogType = actor.dialogType
                     }
                     dialogue = dialogueData
                 }

+ 5 - 0
src/main/java/com/bomostory/sceneeditmodule/screen/view/MovieEditBtnPanelView.kt

@@ -36,6 +36,11 @@ class MovieEditBtnPanelView : ConstraintLayout {
             save.isEnabled = value
         }
 
+    var isLoopEnable = false
+        set(value) {
+            loopSwitch.isChecked = value
+        }
+
     var onAddMusicClickListener: OnClickListener? = null
         set(value) {
             addMusic.setOnClickListener(value)

+ 4 - 0
src/main/java/com/bomostory/sceneeditmodule/screen/view/MovieEditView.kt

@@ -34,6 +34,9 @@ class MovieEditView : ConstraintLayout {
     var currentAudioPosition = 0
         set(value) {
             audioTrackGroupView.currentPosition = value
+            if (musics.size > 0) {
+                movieEditBtnPanel.isLoopEnable = musics[value].isLoop
+            }
         }
 
     var onPlayClickListener: OnClickListener? = null
@@ -98,6 +101,7 @@ class MovieEditView : ConstraintLayout {
 
     var musics = ArrayList<Music>()
         set(value) {
+            field = value
             movieEditBtnPanel.isMusicEmpty = value.isEmpty()
             audioTrackGroupView.musics = value
         }

+ 8 - 1
src/main/java/com/bomostory/sceneeditmodule/share/ExportPdfDialog.kt

@@ -78,8 +78,15 @@ class ExportPdfDialog : DialogFragment() {
         val width = context.resources.getDimension(R.dimen.export_pdf_cover_width).toInt()
         val height = context.resources.getDimension(R.dimen.export_pdf_cover_height).toInt()
         val name = project.name ?: ""
+        val author = project.author ?: ""
         val color = project.frontCoverColor.getColor(context)
-        return CoverDrawer.drawFrontCover(width, height, name, color)
+        project.coverFile.let { coverFile ->
+            return if (coverFile == null) {
+                CoverDrawer.drawFrontCover(width, height, name, author, color)
+            } else {
+                CoverDrawer.drawFrontCover(width, height, name, author, coverFile)
+            }
+        }
     }
 
     private fun createBackCover(context: Context): Bitmap {

+ 2 - 2
src/main/java/com/bomostory/sceneeditmodule/utils/MusicPlayerV2.kt

@@ -6,7 +6,7 @@ import java.util.*
 
 class MusicPlayerV2 {
     interface OnPlayChangedListener {
-        fun OnPlayChanged(position: Int)
+        fun onPlayChanged(position: Int)
     }
 
     lateinit var music: Music
@@ -69,7 +69,7 @@ class MusicPlayerV2 {
         timer?.scheduleAtFixedRate(object : TimerTask() {
             override fun run() {
                 if (!isPause) {
-                    playChangedListener?.OnPlayChanged(mediaPlayer.currentPosition)
+                    playChangedListener?.onPlayChanged(mediaPlayer.currentPosition)
                 }
             }
         }, 0, 1)

+ 1 - 2
src/main/res/layout/dialog_export_pdf.xml

@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="800dp"
     android:layout_height="544dp"
     android:background="@drawable/bg_rounded_8dp"
@@ -86,7 +85,7 @@
         android:layout_height="wrap_content"
         android:layout_marginTop="136dp"
         android:buttonTint="@color/cocoa"
-        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintLeft_toLeftOf="@id/rb_exportPdfDialog_standard"
         app:layout_constraintTop_toBottomOf="@id/rb_exportPdfDialog_standard" />
 
     <TextView

+ 4 - 4
src/main/res/layout/view_front_cover_editor.xml

@@ -34,11 +34,11 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginTop="16dp"
-        android:ellipsize="middle"
+        android:ellipsize="end"
         android:fontFamily="sans-serif-medium"
         android:gravity="center"
         android:singleLine="true"
-        android:textColor="#ffffff"
+        android:textColor="#000000"
         android:textSize="40sp"
         app:layout_constraintLeft_toLeftOf="@id/iv_frontCoverEditor_coverMask"
         app:layout_constraintRight_toRightOf="@id/iv_frontCoverEditor_coverMask"
@@ -50,11 +50,11 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginTop="9dp"
-        android:ellipsize="middle"
+        android:ellipsize="end"
         android:fontFamily="sans-serif-medium"
         android:gravity="center"
         android:singleLine="true"
-        android:textColor="#ffffff"
+        android:textColor="#000000"
         android:textSize="20sp"
         app:layout_constraintLeft_toLeftOf="@id/iv_frontCoverEditor_coverMask"
         app:layout_constraintRight_toRightOf="@id/iv_frontCoverEditor_coverMask"

+ 4 - 4
src/main/res/layout/view_front_cover_scene_chooser.xml

@@ -59,11 +59,11 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginTop="16dp"
-        android:ellipsize="middle"
+        android:ellipsize="end"
         android:fontFamily="sans-serif-medium"
         android:gravity="center"
         android:singleLine="true"
-        android:textColor="#ffffff"
+        android:textColor="#000000"
         android:textSize="40sp"
         app:layout_constraintLeft_toLeftOf="@id/iv_frontCoverSceneChooserDialog_coverMask"
         app:layout_constraintRight_toRightOf="@id/iv_frontCoverSceneChooserDialog_coverMask"
@@ -75,11 +75,11 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginTop="9dp"
-        android:ellipsize="middle"
+        android:ellipsize="end"
         android:fontFamily="sans-serif-medium"
         android:gravity="center"
         android:singleLine="true"
-        android:textColor="#ffffff"
+        android:textColor="#000000"
         android:textSize="20sp"
         app:layout_constraintLeft_toLeftOf="@id/iv_frontCoverSceneChooserDialog_coverMask"
         app:layout_constraintRight_toRightOf="@id/iv_frontCoverSceneChooserDialog_coverMask"