You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt

395 lines
15 KiB
Kotlin

/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.activity
import android.app.Activity
import android.content.Context
import android.graphics.Paint
import android.net.Uri
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.notes.CommentsController
import com.todoroo.astrid.repeats.RepeatControlSet
import com.todoroo.astrid.timers.TimerPlugin
import com.todoroo.astrid.ui.StartDateControlSet
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
3 years ago
import org.tasks.data.Alarm
import org.tasks.data.Location
import org.tasks.data.TagData
import org.tasks.data.UserActivity
import org.tasks.data.UserActivityDao
import org.tasks.databinding.FragmentTaskEditBinding
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.Linkify
import org.tasks.files.FileHelper
import org.tasks.fragments.TaskEditControlSetFragmentManager
import org.tasks.markdown.MarkdownProvider
import org.tasks.notifications.NotificationManager
import org.tasks.preferences.Preferences
import org.tasks.ui.SubtaskControlSet
import org.tasks.ui.TaskEditControlFragment
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskEditViewModel
import javax.inject.Inject
import kotlin.math.abs
@AndroidEntryPoint
class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var userActivityDao: UserActivityDao
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var context: Activity
@Inject lateinit var taskEditControlSetFragmentManager: TaskEditControlSetFragmentManager
@Inject lateinit var commentsController: CommentsController
@Inject lateinit var preferences: Preferences
@Inject lateinit var firebase: Firebase
@Inject lateinit var timerPlugin: TimerPlugin
@Inject lateinit var linkify: Linkify
@Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var taskEditEventBus: TaskEditEventBus
private val linkifyEnabled: Boolean
get() = preferences.getBoolean(R.string.p_linkify_task_edit, false)
val editViewModel: TaskEditViewModel by viewModels()
lateinit var binding: FragmentTaskEditBinding
private var showKeyboard = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val args = requireArguments()
editViewModel.setup(
args.getParcelable(EXTRA_TASK)!!,
args.getParcelable(EXTRA_LIST)!!,
args.getParcelable(EXTRA_LOCATION),
args.getParcelableArrayList(EXTRA_TAGS)!!,
args.getParcelableArrayList(EXTRA_ALARMS)!!
)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentTaskEditBinding.inflate(inflater)
val view: View = binding.root
val model = editViewModel.task!!
val toolbar = binding.toolbar
toolbar.navigationIcon = context.getDrawable(R.drawable.ic_outline_save_24px)
toolbar.setNavigationOnClickListener {
lifecycleScope.launch {
save()
}
}
val backButtonSavesTask = preferences.backButtonSavesTask()
toolbar.setNavigationContentDescription(if (backButtonSavesTask) {
R.string.discard
} else {
R.string.save
})
toolbar.inflateMenu(R.menu.menu_task_edit_fragment)
val menu = toolbar.menu
val delete = menu.findItem(R.id.menu_delete)
delete.isVisible = !model.isNew
delete.setShowAsAction(
if (backButtonSavesTask) MenuItem.SHOW_AS_ACTION_NEVER else MenuItem.SHOW_AS_ACTION_IF_ROOM)
val discard = menu.findItem(R.id.menu_discard)
discard.isVisible = backButtonSavesTask
discard.setShowAsAction(
if (model.isNew) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER)
if (savedInstanceState == null) {
showKeyboard = model.isNew && isNullOrEmpty(model.title)
}
val params = binding.appbarlayout.layoutParams as CoordinatorLayout.LayoutParams
params.behavior = AppBarLayout.Behavior()
val behavior = params.behavior as AppBarLayout.Behavior?
behavior!!.setDragCallback(object : DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
return false
}
})
toolbar.setOnMenuItemClickListener(this)
val title = binding.title
val textWatcher = markdownProvider.markdown(linkifyEnabled).textWatcher(title)
title.addTextChangedListener(
onTextChanged = { _, _, _, _ ->
editViewModel.title = title.text.toString().trim { it <= ' ' }
},
afterTextChanged = {
textWatcher?.invoke(it)
}
)
title.setText(model.title)
title.setHorizontallyScrolling(false)
title.maxLines = 5
if (model.isNew || preferences.getBoolean(R.string.p_hide_check_button, false)) {
binding.fab.visibility = View.INVISIBLE
} else if (editViewModel.completed!!) {
title.paintFlags = title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
binding.fab.setImageResource(R.drawable.ic_outline_check_box_outline_blank_24px)
}
binding.fab.setOnClickListener {
if (editViewModel.completed!!) {
editViewModel.completed = false
title.paintFlags = title.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
binding.fab.setImageResource(R.drawable.ic_outline_check_box_24px)
} else {
editViewModel.completed = true
lifecycleScope.launch {
save()
}
}
}
binding.appbarlayout.addOnOffsetChangedListener(
OnOffsetChangedListener { appBarLayout: AppBarLayout, verticalOffset: Int ->
if (verticalOffset == 0) {
title.visibility = View.VISIBLE
binding.collapsingtoolbarlayout.isTitleEnabled = false
} else if (abs(verticalOffset) < appBarLayout.totalScrollRange) {
title.visibility = View.INVISIBLE
binding.collapsingtoolbarlayout.title = title.text
binding.collapsingtoolbarlayout.isTitleEnabled = true
}
})
if (!model.isNew) {
lifecycleScope.launch {
notificationManager.cancel(model.id)
}
if (linkifyEnabled) {
linkify.linkify(title)
}
}
commentsController.initialize(model, binding.comments)
commentsController.reloadView()
val fragmentManager = childFragmentManager
val taskEditControlFragments =
taskEditControlSetFragmentManager.getOrCreateFragments(fragmentManager)
val visibleSize = taskEditControlSetFragmentManager.visibleSize
val fragmentTransaction = fragmentManager.beginTransaction()
for (i in taskEditControlFragments.indices) {
val taskEditControlFragment = taskEditControlFragments[i]
val tag = getString(taskEditControlFragment.controlId())
fragmentTransaction.replace(
TaskEditControlSetFragmentManager.TASK_EDIT_CONTROL_FRAGMENT_ROWS[i],
taskEditControlFragment,
tag)
if (i >= visibleSize) {
fragmentTransaction.hide(taskEditControlFragment)
}
}
fragmentTransaction.commit()
for (i in visibleSize - 1 downTo 1) {
binding.controlSets.addView(inflater.inflate(R.layout.task_edit_row_divider, binding.controlSets, false), i)
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
taskEditEventBus
.onEach(this::process)
.launchIn(viewLifecycleOwner.lifecycleScope)
}
private suspend fun process(event: TaskEditEvent) {
when (event) {
is TaskEditEvent.Discard ->
if (event.id == editViewModel.task.id) {
editViewModel.discard()
}
}
}
override fun onResume() {
super.onResume()
if (showKeyboard) {
binding.title.requestFocus()
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.title, InputMethodManager.SHOW_IMPLICIT)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
AndroidUtilities.hideKeyboard(activity)
if (item.itemId == R.id.menu_delete) {
deleteButtonClick()
return true
} else if (item.itemId == R.id.menu_discard) {
discardButtonClick()
return true
}
return false
}
suspend fun stopTimer(): Task {
val model = editViewModel.task!!
timerPlugin.stopTimer(model)
val elapsedTime = DateUtils.formatElapsedTime(model.elapsedSeconds.toLong())
addComment(String.format(
"%s %s\n%s %s", // $NON-NLS-1$
getString(R.string.TEA_timer_comment_stopped),
DateUtilities.getTimeString(context, newDateTime()),
getString(R.string.TEA_timer_comment_spent),
elapsedTime),
null)
return model
}
suspend fun startTimer(): Task {
val model = editViewModel.task!!
timerPlugin.startTimer(model)
addComment(String.format(
"%s %s",
getString(R.string.TEA_timer_comment_started),
DateUtilities.getTimeString(context, newDateTime())),
null)
return model
}
suspend fun save() = editViewModel.save()
/*
* ======================================================================
* =============================================== model reading / saving
* ======================================================================
*/
private val repeatControlSet: RepeatControlSet?
get() = getFragment<RepeatControlSet>(RepeatControlSet.TAG)
private val subtaskControlSet: SubtaskControlSet?
get() = getFragment<SubtaskControlSet>(SubtaskControlSet.TAG)
private val startDateControlSet: StartDateControlSet?
get() = getFragment<StartDateControlSet>(StartDateControlSet.TAG)
private fun <T : TaskEditControlFragment?> getFragment(tag: Int): T? {
return childFragmentManager.findFragmentByTag(getString(tag)) as T?
}
/*
* ======================================================================
* ======================================================= event handlers
* ======================================================================
*/
fun discardButtonClick() {
if (editViewModel.hasChanges()) {
dialogBuilder
.newDialog(R.string.discard_confirmation)
.setPositiveButton(R.string.keep_editing, null)
.setNegativeButton(R.string.discard) { _, _ -> discard() }
.show()
} else {
discard()
}
}
private fun deleteButtonClick() {
dialogBuilder
.newDialog(R.string.DLG_delete_this_task_question)
.setPositiveButton(R.string.ok) { _, _ -> delete() }
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun discard() = lifecycleScope.launch {
editViewModel.discard()
}
private fun delete() = lifecycleScope.launch {
editViewModel.delete()
}
/*
* ======================================================================
* ========================================== UI component helper classes
* ======================================================================
*/
fun onDueDateChanged() {
repeatControlSet?.onDueDateChanged()
startDateControlSet?.onDueDateChanged()
}
fun onRemoteListChanged(filter: Filter?) {
subtaskControlSet?.onRemoteListChanged(filter)
}
fun addComment(message: String?, picture: Uri?) {
val model = editViewModel.task!!
val userActivity = UserActivity()
if (picture != null) {
val output = FileHelper.copyToUri(context, preferences.attachmentsDirectory!!, picture)
userActivity.setPicture(output)
}
userActivity.message = message
userActivity.targetId = model.uuid
userActivity.created = DateUtilities.now()
lifecycleScope.launch {
withContext(NonCancellable) {
userActivityDao.createNew(userActivity)
}
commentsController.reloadView()
}
}
companion object {
const val TAG_TASKEDIT_FRAGMENT = "taskedit_fragment"
private const val EXTRA_TASK = "extra_task"
private const val EXTRA_LIST = "extra_list"
private const val EXTRA_LOCATION = "extra_location"
private const val EXTRA_TAGS = "extra_tags"
private const val EXTRA_ALARMS = "extra_alarms"
fun newTaskEditFragment(
task: Task,
list: Filter,
location: Location?,
tags: ArrayList<TagData>,
alarms: ArrayList<Alarm>,
3 years ago
): TaskEditFragment {
val taskEditFragment = TaskEditFragment()
val arguments = Bundle()
arguments.putParcelable(EXTRA_TASK, task)
arguments.putParcelable(EXTRA_LIST, list)
arguments.putParcelable(EXTRA_LOCATION, location)
arguments.putParcelableArrayList(EXTRA_TAGS, tags)
arguments.putParcelableArrayList(EXTRA_ALARMS, alarms)
taskEditFragment.arguments = arguments
return taskEditFragment
}
}
}