Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge stories creation save frame refactor #257

Merged
merged 73 commits into from
Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
b992c6a
replaced SaveButton with NextButton
mzorz Jan 13, 2020
b032fde
added containsAnyAddedViewsOfType method
mzorz Jan 13, 2020
753638c
added getImmutableCurrentStoryFrames() method for 1:1 access to the R…
mzorz Jan 13, 2020
ebf75bf
removed redundant anyStickersAdded method from PhotoEditor, and added…
mzorz Jan 13, 2020
270cabe
temporarily commented out code to split into PRs
mzorz Jan 13, 2020
0050718
fixed merge conflict
mzorz Jan 13, 2020
952ae0d
moved file writing code to FileUtils
mzorz Jan 14, 2020
a73e6c2
added FrameSaveManager to handle saving a Story to disk
mzorz Jan 15, 2020
1109887
cleaned up test code a bit, I now know the background images is chang…
mzorz Jan 15, 2020
dca3d52
removing PhotoEditorView layout transition animation then readding it…
mzorz Jan 15, 2020
9a80718
fixed issue with keeping up with added views when switching frames
mzorz Jan 15, 2020
49baf3a
setting background image directly as opposed to using asynchronous Gl…
mzorz Jan 15, 2020
70cc7d2
fixed lint warnings
mzorz Jan 15, 2020
95078bf
removed no longer used saveImage method
mzorz Jan 15, 2020
d577be9
added TODO
mzorz Jan 15, 2020
eacb43b
removed unneeded context parameter
mzorz Jan 16, 2020
bdb75aa
removed commented line
mzorz Jan 16, 2020
5a7e88e
renamed method
mzorz Jan 16, 2020
00a505c
add new method to scan media files once the full Story is ready
mzorz Jan 16, 2020
6483653
removed unneeded interrmediate ArrayList
mzorz Jan 16, 2020
7d1614e
create a ghost PhotoEditorView to work offscreen for capturing frames
mzorz Jan 16, 2020
86cc611
now creating then 'ghost' offscreen PhotoEditorView within preparePho…
mzorz Jan 16, 2020
ea3fa2d
fixed lint warning
mzorz Jan 16, 2020
c0180da
Merge branch 'feature/stories-creation-save-frame-refactor' into feat…
mzorz Jan 16, 2020
96bee0a
removed commented code
mzorz Jan 16, 2020
156de3f
added async/awaitAll to launch several frames to be saved concurrently
mzorz Jan 17, 2020
841d71c
now creating the ghost (offscreen) PhotoEditorView only once per Story
mzorz Jan 17, 2020
ecd4300
fixed merge conflict
mzorz Jan 17, 2020
8419b2f
moved saveLoopFrame, saveImageFrame and saveStory fun to FrameSaveMan…
mzorz Jan 20, 2020
9624037
passing frame sequenceId (index) to avoid obtaining the same file in …
mzorz Jan 20, 2020
e76cb0e
moved offscreen ghost PhotoEditorView creation within async block to …
mzorz Jan 20, 2020
51dd169
dispatching potentially long running saveStory in the background thre…
mzorz Jan 21, 2020
6e54cfe
removed commented code
mzorz Jan 21, 2020
0c969ce
changed dispatcher for background (Default) for FrameSaveManager
mzorz Jan 21, 2020
ac95f0c
simplified and removed a withContext switch in Activity
mzorz Jan 21, 2020
ab633da
added a text for user's feedback
mzorz Jan 21, 2020
ccaec7d
removed unused import
mzorz Jan 21, 2020
ebb7197
cancelling saveStory job in the case the Activity gets destroyed
mzorz Jan 21, 2020
50be144
added cloneViewSpecs method
mzorz Jan 21, 2020
4630e74
running addView / removeView on the Main thread, and usiing cloneView…
mzorz Jan 21, 2020
8863cac
fixed lint warning
mzorz Jan 21, 2020
46b5d2a
added call to yield() to make the coroutine cancellable
mzorz Jan 21, 2020
2f8f6d8
Removing unused and related set of functions from PhotoEditor, given…
mzorz Jan 22, 2020
da01b6b
now PhotoEditor knows how to save an image from PhotoEditorView, whil…
mzorz Jan 22, 2020
067ee62
made BackgroundSource a sealed class to make it easier to compare, an…
mzorz Jan 23, 2020
a38126e
removed commented code
mzorz Jan 23, 2020
4d16afe
fixed lint warnings
mzorz Jan 23, 2020
6fbcfe9
fixed source handling for static background
mzorz Jan 23, 2020
cd04ec2
added TODOs for error handling when saving video
mzorz Jan 24, 2020
2ea7362
Merge pull request #251 from Automattic/feature/stories-creation-save…
aforcier Jan 25, 2020
40e1bf0
Merge pull request #252 from Automattic/feature/stories-creation-save…
aforcier Jan 25, 2020
c87d27e
simplified if/else block with kotlin expression
mzorz Jan 26, 2020
cf042aa
duh - awaitAll already returns a List<File>
mzorz Jan 28, 2020
e9b289b
avoid ambiguous context by explicitely setting deferreds async calls …
mzorz Jan 28, 2020
c295235
simplified loop using mapIndexed
mzorz Jan 28, 2020
880bb07
removed unused import
mzorz Jan 28, 2020
019e7b5
Merge branch 'feature/stories-creation-save-frame-refactor-part3' int…
mzorz Jan 28, 2020
1e3657e
fixed merge conflict and made sure resulting file list is null-filtered
mzorz Jan 28, 2020
3463f7d
added refresh story frame selection method to re-add views after stor…
mzorz Jan 28, 2020
fe546ab
removed lateinit var for val and return@withContext
mzorz Jan 28, 2020
7ce8957
Merge branch 'feature/stories-creation-save-frame-refactor-part3' int…
mzorz Jan 28, 2020
073b317
fixed merge conflict
mzorz Jan 28, 2020
017a517
Merge branch 'feature/stories-creation-save-frame-refactor-part5' int…
mzorz Jan 28, 2020
220c487
fixed: was wronlgy wrapping async around withContext when it needed b…
mzorz Jan 29, 2020
aa61440
Merge branch 'feature/stories-creation-save-frame-refactor-part3' int…
mzorz Jan 29, 2020
647cce7
Merge branch 'feature/stories-creation-save-frame-refactor-part4' int…
mzorz Jan 29, 2020
19bfa63
Merge branch 'feature/stories-creation-save-frame-refactor-part5' int…
mzorz Jan 29, 2020
1c0edf9
Merge pull request #253 from Automattic/feature/stories-creation-save…
aforcier Jan 29, 2020
314e76e
Merge pull request #254 from Automattic/feature/stories-creation-save…
aforcier Jan 30, 2020
185ce71
simplified expression in if block
mzorz Jan 31, 2020
f69a58e
made val private
mzorz Jan 31, 2020
ea3c898
Merge pull request #255 from Automattic/feature/stories-creation-save…
aforcier Jan 31, 2020
8915ff8
Merge pull request #256 from Automattic/feature/stories-creation-save…
aforcier Jan 31, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package com.automattic.portkey.compose.frame

import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Color
import android.net.Uri
import android.view.ViewGroup.LayoutParams
import android.widget.RelativeLayout
import com.automattic.photoeditor.PhotoEditor
import com.automattic.photoeditor.PhotoEditor.OnSaveWithCancelListener
import com.automattic.photoeditor.views.PhotoEditorView
import com.automattic.photoeditor.views.ViewType.STICKER_ANIMATED
import com.automattic.portkey.compose.story.StoryFrameItem
import com.automattic.portkey.compose.story.StoryFrameItem.BackgroundSource.FileBackgroundSource
import com.automattic.portkey.compose.story.StoryFrameItem.BackgroundSource.UriBackgroundSource
import com.automattic.portkey.compose.story.StoryFrameItemType.IMAGE
import com.automattic.portkey.compose.story.StoryFrameItemType.VIDEO
import com.automattic.portkey.util.cloneViewSpecs
import com.automattic.portkey.util.removeViewFromParent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import java.io.File
import kotlin.coroutines.CoroutineContext

class FrameSaveManager(private val photoEditor: PhotoEditor) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + job

// TODO: not sure whether we really want to cancel a Story frame saving operation, but for now I'll let this
// one in to be a good citizen with Activity / CoroutineScope
fun onCancel() {
job.cancel()
}

suspend fun saveStory(
context: Context,
frames: List<StoryFrameItem>
): List<File?> {
// first, launch all frame save processes async
return frames.mapIndexed { index, frame ->
withContext(coroutineContext) {
async {
yield()
saveLoopFrame(context, frame, index)
}
}
}.awaitAll()
}

private suspend fun saveLoopFrame(
context: Context,
frame: StoryFrameItem,
sequenceId: Int
): File? {
var frameFile: File? = null
when (frame.frameItemType) {
VIDEO -> {
frameFile = saveVideoFrame(frame, sequenceId)
}
IMAGE -> {
// check whether there are any GIF stickers - if there are, we need to produce a video instead
if (frame.addedViews.containsAnyAddedViewsOfType(STICKER_ANIMATED)) {
// TODO make saveVideoWithStaticBackground return File
// saveVideoWithStaticBackground()
} else {
// create ghost PhotoEditorView to be used for saving off-screen
val ghostPhotoEditorView = createGhostPhotoEditor(context, photoEditor.composedCanvas)
frameFile = saveImageFrame(frame, ghostPhotoEditorView, sequenceId)
}
}
}
return frameFile
}

private suspend fun saveImageFrame(
frame: StoryFrameItem,
ghostPhotoEditorView: PhotoEditorView,
sequenceId: Int
): File {
// prepare the ghostview with its background image and the AddedViews on top of it
preparePhotoEditorViewForSnapshot(frame, ghostPhotoEditorView)
val file = withContext(Dispatchers.IO) {
return@withContext photoEditor.saveImageFromPhotoEditorViewAsLoopFrameFile(sequenceId, ghostPhotoEditorView)
}

withContext(Dispatchers.Main) {
// don't forget to remove these views from ghost offscreen view before exiting
for (oneView in frame.addedViews) {
removeViewFromParent(oneView.view)
}
}
return file
}

private suspend fun saveVideoFrame(
frame: StoryFrameItem,
sequenceId: Int
): File? {
var file: File? = null

withContext(Dispatchers.IO) {
var listenerDone = false
val saveListener = object : PhotoEditor.OnSaveWithCancelListener {
override fun onCancel(noAddedViews: Boolean) {
// TODO: error handling
listenerDone = true
}

override fun onSuccess(filePath: String) {
// all good here, continue success path
file = File(filePath)
listenerDone = true
}

override fun onFailure(exception: Exception) {
// TODO: error handling
listenerDone = true
}
}

if (saveVideoAsLoopFrameFile(frame, sequenceId, saveListener)) {
// don't return until we get a signal in the listener
while (!listenerDone) {
delay(100)
}
}
}

return file
}

private fun saveVideoAsLoopFrameFile(
frame: StoryFrameItem,
sequenceId: Int,
onSaveListener: OnSaveWithCancelListener
): Boolean {
var callMade = false
val uri: Uri? = (frame.source as? UriBackgroundSource)?.contentUri
?: Uri.parse((frame.source as FileBackgroundSource).file?.absolutePath)
// we only need the width and height of a model canvas, not creating a canvas clone in the case of videos
// as these are all processed in the background
uri?.let {
photoEditor.saveVideoAsLoopFrameFile(
sequenceId,
it,
photoEditor.composedCanvas.width,
photoEditor.composedCanvas.height,
frame.addedViews,
onSaveListener
)
callMade = true
}
return callMade
}

private suspend fun preparePhotoEditorViewForSnapshot(
frame: StoryFrameItem,
ghostPhotoEditorView: PhotoEditorView
) {
// prepare background
if (frame.source is FileBackgroundSource) {
frame.source.file?.let {
ghostPhotoEditorView.source.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
}
}
if (frame.source is UriBackgroundSource) {
frame.source.contentUri?.let {
ghostPhotoEditorView.source.setImageURI(it)
}
}

// removeViewFromParent for views that were added in the UI thread need to also run on the main thread
// otherwise we'd get a android.view.ViewRootImpl$CalledFromWrongThreadException:
// Only the original thread that created a view hierarchy can touch its views.
withContext(Dispatchers.Main) {
// now call addViewToParent the addedViews remembered by this frame
for (oneView in frame.addedViews) {
removeViewFromParent(oneView.view)
ghostPhotoEditorView.addView(oneView.view, getViewLayoutParams())
}
}
}

private fun getViewLayoutParams(): LayoutParams {
val params = RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT
)
params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE)
return params
}

private fun createGhostPhotoEditor(context: Context, originalPhotoEditorView: PhotoEditorView): PhotoEditorView {
val ghostPhotoView = PhotoEditorView(context)
cloneViewSpecs(context, originalPhotoEditorView, ghostPhotoView)
ghostPhotoView.setBackgroundColor(Color.BLACK)
return ghostPhotoView
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,8 @@ data class StoryFrameItem(
val frameItemType: StoryFrameItemType = IMAGE,
var addedViews: AddedViewList = AddedViewList()
) {
class BackgroundSource {
var file: File? = null
var contentUri: Uri? = null

constructor (file: File) {
this.file = file
}
constructor(contentUri: Uri) {
this.contentUri = contentUri
}

fun isUri(): Boolean {
return contentUri != null
}

fun isFile(): Boolean {
return file != null
}

fun isDefault(): Boolean {
return isFile() && file == DEFAULT_SOURCE
}

companion object {
private val DEFAULT_SOURCE = File("")
fun getDefault(): BackgroundSource {
return BackgroundSource(DEFAULT_SOURCE)
}
}
sealed class BackgroundSource {
data class UriBackgroundSource(var contentUri: Uri? = null) : BackgroundSource()
data class FileBackgroundSource(var file: File? = null) : BackgroundSource()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.automattic.portkey.compose.story
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.automattic.portkey.compose.story.StoryFrameItem.BackgroundSource.FileBackgroundSource
import com.automattic.portkey.compose.story.StoryFrameItem.BackgroundSource.UriBackgroundSource
import com.automattic.portkey.compose.story.StoryViewModel.StoryFrameListItemUiState.StoryFrameListItemUiStateFrame
import com.automattic.portkey.compose.story.StoryViewModel.StoryFrameListItemUiState.StoryFrameListItemUiStatePlusIcon
import com.automattic.portkey.util.SingleLiveEvent
Expand Down Expand Up @@ -86,6 +88,10 @@ class StoryViewModel(private val repository: StoryRepository, val storyIndex: In
return repository.getCurrentStorySize()
}

fun getImmutableCurrentStoryFrames(): List<StoryFrameItem> {
return repository.getImmutableCurrentStoryFrames()
}

fun anyOfCurrentStoryFramesHasViews(): Boolean {
val frames = repository.getImmutableCurrentStoryFrames()
for (frame in frames) {
Expand Down Expand Up @@ -171,7 +177,11 @@ class StoryViewModel(private val repository: StoryRepository, val storyIndex: In
storyItems.forEachIndexed { index, model ->
val isSelected = (getSelectedFrameIndex() == index)
val filePath =
if (model.source.isUri()) model.source.contentUri.toString() else model.source.file.toString()
if ((model.source is UriBackgroundSource)) {
model.source.contentUri.toString()
} else {
(model.source as FileBackgroundSource).file.toString()
}
val oneFrameUiState = StoryFrameListItemUiStateFrame(
selected = isSelected, filePath = filePath
)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/com/automattic/portkey/util/ViewUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.automattic.portkey.util

import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children

fun removeViewFromParent(view: View) {
view.parent?.let {
it as ViewGroup
if (it.children.contains(view)) {
it.removeView(view)
}
}
}

fun cloneViewSpecs(context: Context, originalView: View, targetView: View) {
val originalWidth = originalView.getWidth()
val originalHeight = originalView.getHeight()

val measuredWidth = View.MeasureSpec.makeMeasureSpec(originalWidth, View.MeasureSpec.EXACTLY)
val measuredHeight = View.MeasureSpec.makeMeasureSpec(originalHeight, View.MeasureSpec.EXACTLY)

targetView.measure(measuredWidth, measuredHeight)
targetView.layout(0, 0, targetView.getMeasuredWidth(), targetView.getMeasuredHeight())
}
11 changes: 10 additions & 1 deletion app/src/main/res/layout/content_composer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,15 @@
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
android:background="@color/black_transp">
<TextView
android:id="@+id/operation_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28sp"
android:textColor="@color/white"
android:layout_gravity="center"
android:text="Saving..."/>
</FrameLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<string name="label_remove">Drag here to remove</string>
<string name="label_done">Done</string>
<string name="label_control_next">Next</string>
<string name="label_saving">Saving...</string>

<string name="menu_delete_frame">Delete frame</string>
<string name="dialog_discard_frame_message">Are you sure you want to delete this frame from your Story?</string>
Expand Down
Loading