Add TaskEditViewModel

pull/1051/head
Alex Baker 4 years ago
parent 3cb802a801
commit a73b716419

@ -1,30 +0,0 @@
package org.tasks.ui
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DescriptionControlSetTest {
@Test
fun replaceCRLF() {
assertEquals("aaa\nbbb", DescriptionControlSet.stripCarriageReturns("aaa\r\nbbb"))
}
@Test
fun replaceCR() {
assertEquals("aaa\nbbb", DescriptionControlSet.stripCarriageReturns("aaa\rbbb"))
}
@Test
fun dontReplaceLF() {
assertEquals("aaa\nbbb", DescriptionControlSet.stripCarriageReturns("aaa\nbbb"))
}
@Test
fun checkIfNull() {
assertNull(DescriptionControlSet.stripCarriageReturns(null))
}
}

@ -0,0 +1,64 @@
package org.tasks.ui.editviewmodel
import android.content.Context
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.Database
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.timers.TimerPlugin
import dagger.hilt.android.qualifiers.ApplicationContext
import org.junit.Before
import org.tasks.calendars.CalendarEventProvider
import org.tasks.injection.InjectingTestCase
import org.tasks.location.GeofenceApi
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.PermissivePermissionChecker
import org.tasks.preferences.Preferences
import org.tasks.ui.TaskEditViewModel
import javax.inject.Inject
open class BaseTaskEditViewModelTest : InjectingTestCase() {
@ApplicationContext @Inject lateinit var context: Context
@Inject lateinit var db: Database
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var timerPlugin: TimerPlugin
@Inject lateinit var calendarEventProvider: CalendarEventProvider
@Inject lateinit var gCalHelper: GCalHelper
@Inject lateinit var taskMover: TaskMover
@Inject lateinit var geofenceApi: GeofenceApi
@Inject lateinit var preferences: Preferences
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var alarmService: AlarmService
protected lateinit var viewModel: TaskEditViewModel
@Before
override fun setUp() {
super.setUp()
viewModel = TaskEditViewModel(
context,
taskDao,
taskDeleter,
timerPlugin,
PermissivePermissionChecker(context),
calendarEventProvider,
gCalHelper,
taskMover,
db.locationDao,
geofenceApi,
db.tagDao,
db.tagDataDao,
preferences,
defaultFilterProvider,
db.googleTaskDao,
db.caldavDao,
taskCompleter,
db.alarmDao,
alarmService)
}
}

@ -0,0 +1,47 @@
package org.tasks.ui.editviewmodel
import androidx.test.annotation.UiThreadTest
import com.natpryce.makeiteasy.MakeItEasy
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class PriorityTests : BaseTaskEditViewModelTest() {
@Test
fun changePriorityCausesChange() {
viewModel.setup(TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)))
viewModel.priority = Task.Priority.MEDIUM
Assert.assertTrue(viewModel.hasChanges())
}
@Test
@UiThreadTest
fun applyPriorityChange() = runBlocking {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH))
viewModel.setup(task)
viewModel.priority = Task.Priority.MEDIUM
viewModel.save()
Assert.assertEquals(Task.Priority.MEDIUM, task.priority)
}
@Test
fun noChangeWhenRevertingPriority() {
viewModel.setup(TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)))
viewModel.priority = Task.Priority.MEDIUM
viewModel.priority = Task.Priority.HIGH
Assert.assertFalse(viewModel.hasChanges())
}
}

@ -0,0 +1,91 @@
package org.tasks.ui.editviewmodel
import androidx.test.annotation.UiThreadTest
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class ReminderTests : BaseTaskEditViewModelTest() {
@Test
@UiThreadTest
fun whenDueReminder() = runBlocking {
val task = newTask()
viewModel.setup(task)
viewModel.whenDue = true
viewModel.save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyAtDeadline)
}
@Test
@UiThreadTest
fun whenOverDueReminder() = runBlocking {
val task = newTask()
viewModel.setup(task)
viewModel.whenOverdue = true
viewModel.save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyAfterDeadline)
}
@Test
@UiThreadTest
fun ringFiveTimes() = runBlocking {
val task = newTask()
viewModel.setup(task)
viewModel.ringFiveTimes = true
viewModel.save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeFive)
}
@Test
@UiThreadTest
fun ringNonstop() = runBlocking {
val task = newTask()
viewModel.setup(task)
viewModel.ringNonstop = true
viewModel.save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
}
@Test
@UiThreadTest
fun ringFiveTimesCantRingNonstop() = runBlocking {
val task = newTask()
viewModel.setup(task)
viewModel.ringNonstop = true
viewModel.ringFiveTimes = true
viewModel.save()
assertFalse(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeFive)
}
@Test
@UiThreadTest
fun ringNonStopCantRingFiveTimes() = runBlocking {
val task = newTask()
viewModel.setup(task)
viewModel.ringFiveTimes = true
viewModel.ringNonstop = true
viewModel.save()
assertFalse(taskDao.fetch(task.id)!!.isNotifyModeFive)
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
}
}

@ -0,0 +1,48 @@
package org.tasks.ui.editviewmodel
import androidx.test.annotation.UiThreadTest
import com.google.ical.values.RRule
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class RepeatTests : BaseTaskEditViewModelTest() {
@Test
@UiThreadTest
fun changeRepeatAfterCompletion() = runBlocking {
val task = newTask(with(TaskMaker.RRULE, RRule("RRULE:FREQ=DAILY;INTERVAL=1")))
viewModel.setup(task)
viewModel.repeatAfterCompletion = true
viewModel.save()
assertEquals(
"RRULE:FREQ=DAILY;INTERVAL=1;FROM=COMPLETION",
taskDao.fetch(task.id)!!.recurrence)
}
@Test
@UiThreadTest
fun removeRepeatAfterCompletion() = runBlocking {
val task = newTask()
task.recurrence = "RRULE:FREQ=DAILY;INTERVAL=1;FROM=COMPLETION"
viewModel.setup(task)
viewModel.repeatAfterCompletion = false
viewModel.save()
assertEquals(
"RRULE:FREQ=DAILY;INTERVAL=1",
taskDao.fetch(task.id)!!.recurrence)
}
}

@ -0,0 +1,45 @@
package org.tasks.ui.editviewmodel
import androidx.test.annotation.UiThreadTest
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TaskEditViewModelTest : BaseTaskEditViewModelTest() {
@Test
fun noChangesForNewTask() {
viewModel.setup(newTask())
assertFalse(viewModel.hasChanges())
}
@Test
@UiThreadTest
fun dontSaveTaskWithoutChanges() = runBlocking {
viewModel.setup(newTask())
assertFalse(viewModel.save())
assertTrue(taskDao.getAll().isEmpty())
}
@Test
@UiThreadTest
fun dontSaveTaskTwice() = runBlocking {
viewModel.setup(newTask())
viewModel.priority = Task.Priority.HIGH
assertTrue(viewModel.save())
assertFalse(viewModel.save())
}
}

@ -0,0 +1,48 @@
package org.tasks.ui.editviewmodel
import androidx.test.annotation.UiThreadTest
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task.Priority.Companion.HIGH
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TitleTests : BaseTaskEditViewModelTest() {
@Test
fun changeTitleCausesChange() {
viewModel.setup(newTask())
viewModel.title = "Test"
assertTrue(viewModel.hasChanges())
}
@Test
@UiThreadTest
fun saveWithEmptyTitle() = runBlocking {
val task = newTask()
viewModel.setup(task)
viewModel.priority = HIGH
viewModel.save()
assertEquals("(No title)", taskDao.fetch(task.id)!!.title)
}
@Test
@UiThreadTest
fun newTaskPrepopulatedWithTitleHasChanges() {
viewModel.setup(newTask(with(TaskMaker.TITLE, "some title")))
assertTrue(viewModel.hasChanges())
}
}

@ -27,8 +27,8 @@ object TaskMaker {
val SNOOZE_TIME: Property<Task, DateTime?> = newProperty()
val RRULE: Property<Task, RRule?> = newProperty()
val AFTER_COMPLETE: Property<Task, Boolean> = newProperty()
private val TITLE: Property<Task, String?> = newProperty()
private val PRIORITY: Property<Task, Int> = newProperty()
val TITLE: Property<Task, String?> = newProperty()
val PRIORITY: Property<Task, Int> = newProperty()
val PARENT: Property<Task, Long> = newProperty()
val UUID: Property<Task, String> = newProperty()

@ -20,7 +20,6 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment
import com.todoroo.astrid.activity.TaskEditFragment.TaskEditFragmentCallbackHandler
import com.todoroo.astrid.activity.TaskListFragment.TaskListFragmentCallbackHandler
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.dao.TaskDao
@ -28,8 +27,9 @@ import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.timers.TimerControlSet.TimerControlSetCallback
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.activities.TagSettingsActivity
@ -60,7 +60,7 @@ import org.tasks.ui.NavigationDrawerFragment
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandler, OnListChanged, TimerControlSetCallback, DueDateChangeListener, TaskEditFragmentCallbackHandler, CommentBarFragmentCallback, SortDialogCallback {
class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandler, OnListChanged, TimerControlSetCallback, DueDateChangeListener, CommentBarFragmentCallback, SortDialogCallback {
@Inject lateinit var preferences: Preferences
@Inject lateinit var repeatConfirmationReceiver: RepeatConfirmationReceiver
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@ -175,7 +175,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
val loadFilter = intent.hasExtra(LOAD_FILTER)
val tef = taskEditFragment
if (tef != null && (openFilter || loadFilter)) {
tef.save()
lifecycleScope.launch(NonCancellable) {
tef.save()
}
}
if (loadFilter || !openFilter && filter == null) {
lifecycleScope.launch {
@ -305,15 +307,15 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
if (task == null) {
return
}
val taskEditFragment = taskEditFragment
taskEditFragment?.save()
clearUi()
lifecycleScope.launch {
val list = async { defaultFilterProvider.getList(task) }
val location = async { locationDao.getLocation(task, preferences) }
val tags = async { tagDataDao.getTags(task) }
val fragment = newTaskEditFragment(
task, filterColor, list.await(), location.await(), tags.await())
taskEditFragment?.let {
it.editViewModel.cleared.removeObservers(this@MainActivity)
withContext(NonCancellable) {
it.save()
}
}
clearUi()
val fragment = newTaskEditFragment(task, filterColor)
supportFragmentManager.beginTransaction()
.replace(R.id.detail, fragment, TaskEditFragment.TAG_TASKEDIT_FRAGMENT)
.addToBackStack(TaskEditFragment.TAG_TASKEDIT_FRAGMENT)
@ -332,14 +334,15 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
navigationDrawer.closeDrawer()
return
}
val taskEditFragment = taskEditFragment
if (taskEditFragment != null) {
taskEditFragment?.let {
if (preferences.backButtonSavesTask()) {
taskEditFragment.save()
lifecycleScope.launch(NonCancellable) {
it.save()
}
} else {
taskEditFragment.discardButtonClick()
it.discardButtonClick()
}
return
return@onBackPressed
}
if (taskListFragment?.collapseSearchView() == true) {
return
@ -364,7 +367,7 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
private val isSinglePaneLayout: Boolean
get() = !resources.getBoolean(R.bool.two_pane_layout)
override fun removeTaskEditFragment() {
fun removeTaskEditFragment() {
supportFragmentManager
.popBackStackImmediate(
TaskEditFragment.TAG_TASKEDIT_FRAGMENT, FragmentManager.POP_BACK_STACK_INCLUSIVE)
@ -413,8 +416,8 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
actionMode = null
}
override fun dueDateChanged(dateTime: Long) {
taskEditFragment!!.onDueDateChanged(dateTime)
override fun dueDateChanged() {
taskEditFragment!!.onDueDateChanged()
}
override fun onListChanged(filter: Filter?) {

@ -20,32 +20,30 @@ 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.Observer
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.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
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.service.TaskDeleter
import com.todoroo.astrid.timers.TimerPlugin
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.Event
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
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
@ -59,6 +57,7 @@ import org.tasks.preferences.Preferences
import org.tasks.themes.ThemeColor
import org.tasks.ui.SubtaskControlSet
import org.tasks.ui.TaskEditControlFragment
import org.tasks.ui.TaskEditViewModel
import javax.inject.Inject
import kotlin.math.abs
@ -66,7 +65,6 @@ import kotlin.math.abs
class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var userActivityDao: UserActivityDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var context: Activity
@ -77,32 +75,33 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var timerPlugin: TimerPlugin
@Inject lateinit var linkify: Linkify
lateinit var model: Task
val editViewModel: TaskEditViewModel by viewModels()
lateinit var binding: FragmentTaskEditBinding
private var callback: TaskEditFragmentCallbackHandler? = null
private var showKeyboard = false
private var completed = false
override fun onAttach(activity: Activity) {
super.onAttach(activity)
callback = activity as TaskEditFragmentCallbackHandler
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(EXTRA_COMPLETED, completed)
editViewModel.setup(requireArguments().getParcelable(EXTRA_TASK)!!)
val activity = requireActivity() as MainActivity
editViewModel.cleared.observe(activity, Observer<Event<Boolean>> {
activity.removeTaskEditFragment()
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentTaskEditBinding.inflate(inflater)
val view: View = binding.root
val arguments = requireArguments()
model = arguments.getParcelable(EXTRA_TASK)!!
val themeColor: ThemeColor = arguments.getParcelable(EXTRA_THEME)!!
val model = editViewModel.task!!
val themeColor: ThemeColor = requireArguments().getParcelable(EXTRA_THEME)!!
val toolbar = binding.toolbar
toolbar.navigationIcon = context.getDrawable(R.drawable.ic_outline_save_24px)
toolbar.setNavigationOnClickListener { save() }
toolbar.setNavigationOnClickListener {
lifecycleScope.launch(NonCancellable) {
save()
}
}
val backButtonSavesTask = preferences.backButtonSavesTask()
toolbar.inflateMenu(R.menu.menu_task_edit_fragment)
val menu = toolbar.menu
@ -116,9 +115,6 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (model.isNew) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER)
if (savedInstanceState == null) {
showKeyboard = model.isNew && isNullOrEmpty(model.title)
completed = model.isCompleted
} else {
completed = savedInstanceState.getBoolean(EXTRA_COMPLETED)
}
val params = binding.appbarlayout.layoutParams as CoordinatorLayout.LayoutParams
params.behavior = AppBarLayout.Behavior()
@ -136,20 +132,25 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
title.setTextColor(themeColor.colorOnPrimary)
title.setHintTextColor(themeColor.hintOnPrimary)
title.maxLines = 5
title.addTextChangedListener { text ->
editViewModel.title = text.toString().trim { it <= ' ' }
}
if (model.isNew || preferences.getBoolean(R.string.p_hide_check_button, false)) {
binding.fab.visibility = View.INVISIBLE
} else if (completed) {
} 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 (completed) {
completed = false
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 {
completed = true
save()
editViewModel.completed = true
lifecycleScope.launch(NonCancellable) {
save()
}
}
}
if (AndroidUtilities.atLeastQ()) {
@ -176,7 +177,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
commentsController.reloadView()
val fragmentManager = childFragmentManager
val taskEditControlFragments =
taskEditControlSetFragmentManager.getOrCreateFragments(fragmentManager, model, arguments)
taskEditControlSetFragmentManager.getOrCreateFragments(fragmentManager)
val visibleSize = taskEditControlSetFragmentManager.visibleSize
val fragmentTransaction = fragmentManager.beginTransaction()
for (i in taskEditControlFragments.indices) {
@ -199,6 +200,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onResume() {
super.onResume()
if (showKeyboard) {
binding.title.requestFocus()
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
@ -219,6 +221,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
fun stopTimer(): Task {
val model = editViewModel.task!!
timerPlugin.stopTimer(model)
val elapsedTime = DateUtils.formatElapsedTime(model.elapsedSeconds.toLong())
addComment(String.format(
@ -232,6 +235,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
fun startTimer(): Task {
val model = editViewModel.task!!
timerPlugin.startTimer(model)
addComment(String.format(
"%s %s",
@ -242,39 +246,21 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
/** Save task model from values in UI components */
fun save() {
val fragments = taskEditControlSetFragmentManager.getFragmentsInPersistOrder(childFragmentManager)
lifecycleScope.launch(NonCancellable) {
if (hasChanges(fragments)) {
val isNewTask = model.isNew
val taskListFragment = (activity as MainActivity?)!!.taskListFragment
val title = title
model.title = if (isNullOrEmpty(title)) getString(R.string.no_title) else title
if (completed != model.isCompleted) {
model.completionDate = if (completed) DateUtilities.now() else 0
}
val partition = fragments.partition { it.requiresId() }
partition.second.forEach { it.apply(model) }
if (isNewTask) {
taskDao.createNew(model)
}
partition.first.forEach { it.apply(model) }
taskDao.save(model, null)
if (isNewTask) {
taskListFragment!!.onTaskCreated(model.uuid)
if (!isNullOrEmpty(model.calendarURI)) {
taskListFragment.makeSnackbar(R.string.calendar_event_created, model.title)
.setAction(R.string.action_open) {
val uri = model.calendarURI
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
taskListFragment.startActivity(intent)
}
.show()
}
suspend fun save() {
val saved = editViewModel.save()
if (saved && editViewModel.isNew) {
(activity as MainActivity?)?.taskListFragment?.let { taskListFragment ->
val model = editViewModel.task!!
taskListFragment.onTaskCreated(model.uuid)
if (!isNullOrEmpty(model.calendarURI)) {
taskListFragment.makeSnackbar(R.string.calendar_event_created, model.title)
.setAction(R.string.action_open) {
val uri = model.calendarURI
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
taskListFragment.startActivity(intent)
}
.show()
}
callback!!.removeTaskEditFragment()
} else {
discard()
}
}
}
@ -284,68 +270,37 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
* =============================================== model reading / saving
* ======================================================================
*/
private val repeatControlSet: RepeatControlSet
get() = getFragment<RepeatControlSet>(RepeatControlSet.TAG)!!
private val repeatControlSet: RepeatControlSet?
get() = getFragment<RepeatControlSet>(RepeatControlSet.TAG)
private val subtaskControlSet: SubtaskControlSet
get() = getFragment<SubtaskControlSet>(SubtaskControlSet.TAG)!!
private val subtaskControlSet: SubtaskControlSet?
get() = getFragment<SubtaskControlSet>(SubtaskControlSet.TAG)
private fun <T : TaskEditControlFragment?> getFragment(tag: Int): T? {
return childFragmentManager.findFragmentByTag(getString(tag)) as T?
}
private val title: String
get() = binding.title.text.toString().trim { it <= ' ' }
private suspend fun hasChanges(fragments: List<TaskEditControlFragment>): Boolean {
val newTitle = title
if (newTitle != model.title
|| !model.isNew && completed != model.isCompleted
|| model.isNew && !isNullOrEmpty(newTitle)) {
return true
}
try {
return fragments.any { it.hasChanges(model) }
} catch (e: Exception) {
firebase.reportException(e)
}
return false
}
/*
/*
* ======================================================================
* ======================================================= event handlers
* ======================================================================
*/
fun discardButtonClick() {
val fragments = taskEditControlSetFragmentManager.getFragmentsInPersistOrder(childFragmentManager)
lifecycleScope.launch {
if (hasChanges(fragments)) {
dialogBuilder
.newDialog(R.string.discard_confirmation)
.setPositiveButton(R.string.keep_editing, null)
.setNegativeButton(R.string.discard) { _, _ -> discard() }
.show()
} else {
discard()
}
}
}
fun discard() {
if (model.isNew) {
timerPlugin.stopTimer(model)
}
callback!!.removeTaskEditFragment()
if (editViewModel.hasChanges()) {
dialogBuilder
.newDialog(R.string.discard_confirmation)
.setPositiveButton(R.string.keep_editing, null)
.setNegativeButton(R.string.discard) { _, _ -> editViewModel.discard() }
.show()
} else {
editViewModel.discard()
}
}
private fun deleteButtonClick() {
dialogBuilder
.newDialog(R.string.DLG_delete_this_task_question)
.setPositiveButton(android.R.string.ok) { _, _ ->
taskDeleter.markDeleted(model)
callback!!.removeTaskEditFragment()
}
.setPositiveButton(android.R.string.ok) { _, _ -> editViewModel.delete() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
@ -355,17 +310,16 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
* ========================================== UI component helper classes
* ======================================================================
*/
fun onDueDateChanged(dueDate: Long) {
val repeatControlSet: RepeatControlSet? = repeatControlSet
repeatControlSet?.onDueDateChanged(dueDate)
fun onDueDateChanged() {
repeatControlSet?.onDueDateChanged()
}
fun onRemoteListChanged(filter: Filter?) {
val subtaskControlSet: SubtaskControlSet? = subtaskControlSet
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)
@ -382,35 +336,16 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}
interface TaskEditFragmentCallbackHandler {
fun removeTaskEditFragment()
}
companion object {
const val TAG_TASKEDIT_FRAGMENT = "taskedit_fragment"
private const val EXTRA_TASK = "extra_task"
private const val EXTRA_THEME = "extra_theme"
const val EXTRA_LIST = "extra_list"
const val EXTRA_PLACE = "extra_place"
const val EXTRA_TAGS = "extra_tags"
private const val EXTRA_COMPLETED = "extra_completed"
fun newTaskEditFragment(
task: Task,
themeColor: ThemeColor?,
filter: Filter,
place: Location?,
tags: ArrayList<TagData>): TaskEditFragment {
if (BuildConfig.DEBUG) {
require(filter is GtasksFilter || filter is CaldavFilter)
}
fun newTaskEditFragment(task: Task, themeColor: ThemeColor?): TaskEditFragment {
val taskEditFragment = TaskEditFragment()
val arguments = Bundle()
arguments.putParcelable(EXTRA_TASK, task)
arguments.putParcelable(EXTRA_THEME, themeColor)
arguments.putParcelable(EXTRA_LIST, filter)
arguments.putParcelable(EXTRA_PLACE, place)
arguments.putParcelableArrayList(EXTRA_TAGS, tags)
taskEditFragment.arguments = arguments
return taskEditFragment
}

@ -502,11 +502,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
private fun onTaskDelete(task: Task) {
val activity = activity as MainActivity?
if (activity != null) {
val tef = activity.taskEditFragment
if (tef != null && task.id == tef.model.id) {
tef.discard()
(activity as MainActivity?)?.taskEditFragment?.let {
if (task.id == it.editViewModel.task?.id) {
it.editViewModel.discard()
}
}
timerPlugin.stopTimer(task)

@ -226,6 +226,9 @@ SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtas
if (!task.insignificantChange(original)) {
task.modificationDate = DateUtilities.now()
}
if (task.dueDate != original?.dueDate) {
task.reminderSnooze = 0
}
if (update(task) == 1) {
workManager.afterSave(task, original)
}
@ -245,6 +248,9 @@ SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtas
if (Task.isUuidEmpty(task.remoteId)) {
task.remoteId = UUIDHelper.newUUID()
}
if (BuildConfig.DEBUG) {
require(task.remoteId?.isNotBlank() == true && task.remoteId != "0")
}
val insert = insert(task)
task.id = insert
}

@ -11,7 +11,6 @@ import com.todoroo.andlib.data.Table
import com.todoroo.andlib.sql.Field
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.dao.TaskDaoBlocking
import org.tasks.Strings
import org.tasks.backup.XmlReader
import org.tasks.data.Tag
@ -233,11 +232,11 @@ class Task : Parcelable {
return dueDate < compareTo && !isCompleted
}
fun repeatAfterCompletion(): Boolean = recurrence?.contains("FROM=COMPLETION") ?: false
fun repeatAfterCompletion(): Boolean = recurrence.isRepeatAfterCompletion()
fun sanitizedRecurrence(): String? = getRecurrenceWithoutFrom()?.replace("BYDAY=;".toRegex(), "") // $NON-NLS-1$//$NON-NLS-2$
fun getRecurrenceWithoutFrom(): String? = recurrence?.replace(";?FROM=[^;]*".toRegex(), "")
fun getRecurrenceWithoutFrom(): String? = recurrence.withoutFrom()
fun setDueDateAdjustingHideUntil(newDueDate: Long) {
if (dueDate > 0) {
@ -612,5 +611,9 @@ class Task : Parcelable {
@JvmStatic fun isUuidEmpty(uuid: String?): Boolean {
return NO_UUID == uuid || Strings.isNullOrEmpty(uuid)
}
fun String?.isRepeatAfterCompletion() = this?.contains("FROM=COMPLETION") ?: false
fun String?.withoutFrom(): String? = this?.replace(";?FROM=[^;]*".toRegex(), "")
}
}

@ -16,7 +16,6 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
@ -45,10 +44,8 @@ class FilesControlSet : TaskEditControlFragment() {
@BindView(R.id.add_attachment)
lateinit var addAttachment: TextView
private var taskUuid: String? = null
override fun createView(savedInstanceState: Bundle?) {
taskUuid = task.uuid
val task = viewModel.task!!
if (savedInstanceState == null) {
if (task.hasTransitory(TaskAttachment.KEY)) {
for (uri in (task.getTransitory<ArrayList<Uri>>(TaskAttachment.KEY))!!) {
@ -69,16 +66,12 @@ class FilesControlSet : TaskEditControlFragment() {
AddAttachmentDialog.newAddAttachmentDialog(this).show(parentFragmentManager, FRAG_TAG_ADD_ATTACHMENT_DIALOG)
}
override val layout: Int
get() = R.layout.control_set_files
override val layout = R.layout.control_set_files
override val icon: Int
get() = R.drawable.ic_outline_attachment_24px
override val icon = R.drawable.ic_outline_attachment_24px
override fun controlId() = TAG
override suspend fun apply(task: Task) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AddAttachmentDialog.REQUEST_CAMERA || requestCode == AddAttachmentDialog.REQUEST_AUDIO) {
if (resultCode == Activity.RESULT_OK) {
@ -143,7 +136,10 @@ class FilesControlSet : TaskEditControlFragment() {
}
private fun newAttachment(output: Uri) {
val attachment = TaskAttachment(taskUuid!!, output, FileHelper.getFilename(context, output)!!)
val attachment = TaskAttachment(
viewModel.task!!.uuid,
output,
FileHelper.getFilename(context, output)!!)
lifecycleScope.launch {
taskAttachmentDao.createNew(attachment)
addAttachment(attachment)

@ -83,7 +83,9 @@ public class GCalHelper {
}
}
public Uri createTaskEvent(Task task, ContentValues values) {
public Uri createTaskEvent(Task task, String calendarId) {
ContentValues values = new ContentValues();
values.put(CalendarContract.Events.CALENDAR_ID, calendarId);
return createTaskEvent(task, values, true);
}
@ -127,6 +129,19 @@ public class GCalHelper {
return null;
}
public void updateEvent(String uri, Task task) {
try {
ContentValues updateValues = new ContentValues();
// check if we need to update the item
updateValues.put(CalendarContract.Events.TITLE, task.getTitle());
updateValues.put(CalendarContract.Events.DESCRIPTION, task.getNotes());
createStartAndEndDate(task, updateValues);
cr.update(Uri.parse(uri), updateValues, null, null);
} catch (Exception e) {
Timber.e(e, "Failed to update calendar: %s [%s]", uri, task);
}
}
public void rescheduleRepeatingTask(Task task) {
String taskUri = getTaskEventUri(task);
if (isNullOrEmpty(taskUri)) {

@ -18,20 +18,17 @@ import butterknife.OnItemSelected
import com.google.ical.values.Frequency
import com.google.ical.values.RRule
import com.google.ical.values.WeekdayNum
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.dialogs.DialogBuilder
import org.tasks.repeats.BasicRecurrenceDialog
import org.tasks.repeats.RepeatRuleToString
import org.tasks.themes.Theme
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils
import org.tasks.time.DateTimeUtils.currentTimeMillis
import org.tasks.ui.HiddenTopArrayAdapter
import org.tasks.ui.TaskEditControlFragment
import java.text.ParseException
import java.util.*
import javax.inject.Inject
@ -59,65 +56,33 @@ class RepeatControlSet : TaskEditControlFragment() {
@BindView(R.id.repeatTypeContainer)
lateinit var repeatTypeContainer: LinearLayout
private var rrule: RRule? = null
private lateinit var typeAdapter: HiddenTopArrayAdapter<String>
private var dueDate: Long = 0
private var repeatAfterCompletion = false
fun onSelected(rrule: RRule?) {
this.rrule = rrule
viewModel.rrule = rrule
refreshDisplayView()
}
fun onDueDateChanged(dueDate: Long) {
this.dueDate = if (dueDate > 0) dueDate else DateTimeUtils.currentTimeMillis()
if (rrule != null && rrule!!.freq == Frequency.MONTHLY && rrule!!.byDay.isNotEmpty()) {
val weekdayNum = rrule!!.byDay[0]
val dateTime = DateTime(this.dueDate)
val num: Int
val dayOfWeekInMonth = dateTime.dayOfWeekInMonth
num = if (weekdayNum.num == -1 || dayOfWeekInMonth == 5) {
if (dayOfWeekInMonth == dateTime.maxDayOfWeekInMonth) -1 else dayOfWeekInMonth
} else {
dayOfWeekInMonth
fun onDueDateChanged() {
viewModel.rrule?.let {
if (it.freq == Frequency.MONTHLY && it.byDay.isNotEmpty()) {
val weekdayNum = it.byDay[0]
val dateTime = DateTime(this.dueDate)
val num: Int
val dayOfWeekInMonth = dateTime.dayOfWeekInMonth
num = if (weekdayNum.num == -1 || dayOfWeekInMonth == 5) {
if (dayOfWeekInMonth == dateTime.maxDayOfWeekInMonth) -1 else dayOfWeekInMonth
} else {
dayOfWeekInMonth
}
it.byDay = listOf((WeekdayNum(num, dateTime.weekday)))
viewModel.rrule = it
refreshDisplayView()
}
rrule!!.byDay = listOf((WeekdayNum(num, dateTime.weekday)))
refreshDisplayView()
}
}
override fun createView(savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
repeatAfterCompletion = task.repeatAfterCompletion()
dueDate = task.dueDate
if (dueDate <= 0) {
dueDate = DateTimeUtils.currentTimeMillis()
}
val recurrenceWithoutFrom = task.getRecurrenceWithoutFrom()
if (isNullOrEmpty(recurrenceWithoutFrom)) {
rrule = null
} else {
try {
rrule = RRule(recurrenceWithoutFrom)
rrule!!.until = DateTime(task.repeatUntil).toDateValue()
} catch (e: ParseException) {
rrule = null
}
}
} else {
val recurrence = savedInstanceState.getString(EXTRA_RECURRENCE)
dueDate = savedInstanceState.getLong(EXTRA_DUE_DATE)
rrule = if (isNullOrEmpty(recurrence)) {
null
} else {
try {
RRule(recurrence)
} catch (e: ParseException) {
null
}
}
repeatAfterCompletion = savedInstanceState.getBoolean(EXTRA_REPEAT_AFTER_COMPLETION)
}
repeatTypes.add("")
repeatTypes.addAll(listOf(*resources.getStringArray(R.array.repeat_type)))
typeAdapter = object : HiddenTopArrayAdapter<String>(activity, 0, repeatTypes) {
@ -136,76 +101,42 @@ class RepeatControlSet : TaskEditControlFragment() {
drawable.setTint(activity.getColor(R.color.text_primary))
typeSpinner.setBackgroundDrawable(drawable)
typeSpinner.adapter = typeAdapter
typeSpinner.setSelection(if (repeatAfterCompletion) TYPE_COMPLETION_DATE else TYPE_DUE_DATE)
typeSpinner.setSelection(if (viewModel.repeatAfterCompletion!!) TYPE_COMPLETION_DATE else TYPE_DUE_DATE)
refreshDisplayView()
}
@OnItemSelected(R.id.repeatType)
fun onRepeatTypeChanged(position: Int) {
repeatAfterCompletion = position == TYPE_COMPLETION_DATE
repeatTypes[0] = if (repeatAfterCompletion) repeatTypes[2] else repeatTypes[1]
viewModel.repeatAfterCompletion = position == TYPE_COMPLETION_DATE
repeatTypes[0] = if (viewModel.repeatAfterCompletion!!) repeatTypes[2] else repeatTypes[1]
typeAdapter.notifyDataSetChanged()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_RECURRENCE, if (rrule == null) "" else rrule!!.toIcal())
outState.putBoolean(EXTRA_REPEAT_AFTER_COMPLETION, repeatAfterCompletion)
outState.putLong(EXTRA_DUE_DATE, dueDate)
}
private val dueDate: Long
get() = viewModel.dueDate!!.let { if (it > 0) it else currentTimeMillis() }
override fun onRowClick() {
BasicRecurrenceDialog.newBasicRecurrenceDialog(this, rrule, dueDate)
BasicRecurrenceDialog.newBasicRecurrenceDialog(this, viewModel.rrule, dueDate)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
}
override val isClickable: Boolean
get() = true
override val isClickable = true
override val layout: Int
get() = R.layout.control_set_repeat_display
override val layout = R.layout.control_set_repeat_display
override val icon: Int
get() = R.drawable.ic_outline_repeat_24px
override val icon = R.drawable.ic_outline_repeat_24px
override fun controlId() = TAG
override suspend fun hasChanges(original: Task): Boolean {
val repeatUntil = rrule?.let { DateTime.from(it.until).millis } ?: 0
return recurrenceValue != original.recurrence.orEmpty()
|| original.repeatUntil != repeatUntil
}
override suspend fun apply(task: Task) {
task.repeatUntil = if (rrule == null) 0 else DateTime.from(rrule!!.until).millis
task.recurrence = recurrenceValue
}
private val recurrenceValue: String
get() {
if (rrule == null) {
return ""
}
val copy: RRule = try {
RRule(rrule!!.toIcal())
} catch (e: ParseException) {
return ""
}
copy.until = null
var result = copy.toIcal()
if (repeatAfterCompletion && !isNullOrEmpty(result)) {
result += ";FROM=COMPLETION" // $NON-NLS-1$
}
return result
}
private fun refreshDisplayView() {
if (rrule == null) {
displayView.text = null
repeatTypeContainer.visibility = View.GONE
} else {
displayView.text = repeatRuleToString.toString(rrule)
repeatTypeContainer.visibility = View.VISIBLE
viewModel.rrule.let {
if (it == null) {
displayView.text = null
repeatTypeContainer.visibility = View.GONE
} else {
displayView.text = repeatRuleToString.toString(it)
repeatTypeContainer.visibility = View.VISIBLE
}
}
}
@ -214,8 +145,5 @@ class RepeatControlSet : TaskEditControlFragment() {
private const val TYPE_DUE_DATE = 1
private const val TYPE_COMPLETION_DATE = 2
private const val FRAG_TAG_BASIC_RECURRENCE = "frag_tag_basic_recurrence"
private const val EXTRA_RECURRENCE = "extra_recurrence"
private const val EXTRA_DUE_DATE = "extra_due_date"
private const val EXTRA_REPEAT_AFTER_COMPLETION = "extra_repeat_after_completion"
}
}

@ -4,7 +4,6 @@ import static com.todoroo.andlib.utility.DateUtilities.now;
import static com.todoroo.astrid.helper.UUIDHelper.newUUID;
import static org.tasks.Strings.isNullOrEmpty;
import android.content.ContentValues;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.todoroo.andlib.utility.DateUtilities;
@ -87,7 +86,7 @@ public class TaskCreator {
if (!isNullOrEmpty(task.getTitle())
&& gcalCreateEventEnabled
&& isNullOrEmpty(task.getCalendarURI())) {
Uri calendarUri = gcalHelper.createTaskEvent(task, new ContentValues());
Uri calendarUri = gcalHelper.createTaskEvent(task, preferences.getDefaultCalendar());
task.setCalendarURI(calendarUri.toString());
}

@ -12,18 +12,12 @@ import android.view.View
import android.widget.TextView
import butterknife.BindView
import com.google.android.material.chip.ChipGroup
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.data.TagDao
import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.tags.TagPickerActivity
import org.tasks.ui.ChipProvider
import org.tasks.ui.TaskEditControlFragment
import java.util.*
import javax.inject.Inject
/**
@ -33,8 +27,6 @@ import javax.inject.Inject
*/
@AndroidEntryPoint
class TagsControlSet : TaskEditControlFragment() {
@Inject lateinit var tagDao: TagDao
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var chipProvider: ChipProvider
@BindView(R.id.no_tags)
@ -43,71 +35,43 @@ class TagsControlSet : TaskEditControlFragment() {
@BindView(R.id.chip_group)
lateinit var chipGroup: ChipGroup
private lateinit var originalTags: ArrayList<TagData>
private lateinit var selectedTags: ArrayList<TagData>
override fun createView(savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
originalTags = requireArguments().getParcelableArrayList(TaskEditFragment.EXTRA_TAGS)!!
selectedTags = ArrayList(originalTags)
refreshDisplayView()
} else {
selectedTags = savedInstanceState.getParcelableArrayList(EXTRA_SELECTED_TAGS)!!
originalTags = savedInstanceState.getParcelableArrayList(EXTRA_ORIGINAL_TAGS)!!
refreshDisplayView()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(EXTRA_SELECTED_TAGS, selectedTags)
outState.putParcelableArrayList(EXTRA_ORIGINAL_TAGS, originalTags)
}
override val layout: Int
get() = R.layout.control_set_tags
override suspend fun apply(task: Task) {
if (tagDao.applyTags(task, tagDataDao, selectedTags)) {
task.modificationDate = DateUtilities.now()
}
refreshDisplayView()
}
override fun onRowClick() {
val intent = Intent(context, TagPickerActivity::class.java)
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, selectedTags)
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, viewModel.selectedTags)
startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY)
}
override val isClickable: Boolean
get() = true
override val layout = R.layout.control_set_tags
override val icon: Int
get() = R.drawable.ic_outline_label_24px
override val isClickable = true
override fun controlId() = TAG
override val icon = R.drawable.ic_outline_label_24px
override suspend fun hasChanges(original: Task): Boolean {
return HashSet(originalTags) != HashSet(selectedTags)
}
override fun controlId() = TAG
private fun refreshDisplayView() {
if (selectedTags.isEmpty()) {
chipGroup.visibility = View.GONE
tagsDisplay.visibility = View.VISIBLE
} else {
tagsDisplay.visibility = View.GONE
chipGroup.visibility = View.VISIBLE
chipGroup.removeAllViews()
for (tagData in selectedTags.sortedBy(TagData::name)) {
val chip = chipProvider.newClosableChip(tagData)
chipProvider.apply(chip, tagData)
chip.setOnClickListener { onRowClick() }
chip.setOnCloseIconClickListener {
selectedTags.remove(tagData)
refreshDisplayView()
viewModel.selectedTags?.let { selectedTags ->
if (selectedTags.isEmpty()) {
chipGroup.visibility = View.GONE
tagsDisplay.visibility = View.VISIBLE
} else {
tagsDisplay.visibility = View.GONE
chipGroup.visibility = View.VISIBLE
chipGroup.removeAllViews()
for (tagData in selectedTags.sortedBy(TagData::name)) {
val chip = chipProvider.newClosableChip(tagData)
chipProvider.apply(chip, tagData)
chip.setOnClickListener { onRowClick() }
chip.setOnCloseIconClickListener {
selectedTags.remove(tagData)
refreshDisplayView()
}
chipGroup.addView(chip)
}
chipGroup.addView(chip)
}
}
}
@ -115,7 +79,8 @@ class TagsControlSet : TaskEditControlFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) {
if (resultCode == Activity.RESULT_OK && data != null) {
selectedTags = data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!!
viewModel.selectedTags =
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)
refreshDisplayView()
}
} else {
@ -123,12 +88,8 @@ class TagsControlSet : TaskEditControlFragment() {
}
}
override fun requiresId() = true
companion object {
const val TAG = R.string.TEA_ctrl_lists_pref
private const val EXTRA_ORIGINAL_TAGS = "extra_original_tags"
private const val EXTRA_SELECTED_TAGS = "extra_selected_tags"
private const val REQUEST_TAG_PICKER_ACTIVITY = 10582
}
}

@ -51,28 +51,16 @@ class TimerControlSet : TaskEditControlFragment() {
private lateinit var estimated: TimeDurationControlSet
private lateinit var elapsed: TimeDurationControlSet
private var timerStarted: Long = 0
private var dialog: AlertDialog? = null
private lateinit var dialogView: View
private lateinit var callback: TimerControlSetCallback
override fun createView(savedInstanceState: Bundle?) {
val elapsedSeconds: Int
val estimatedSeconds: Int
if (savedInstanceState == null) {
timerStarted = task.timerStart
elapsedSeconds = task.elapsedSeconds
estimatedSeconds = task.estimatedSeconds
} else {
timerStarted = savedInstanceState.getLong(EXTRA_STARTED)
elapsedSeconds = savedInstanceState.getInt(EXTRA_ELAPSED)
estimatedSeconds = savedInstanceState.getInt(EXTRA_ESTIMATED)
}
dialogView = activity.layoutInflater.inflate(R.layout.control_set_timers_dialog, null)
estimated = TimeDurationControlSet(activity, dialogView, R.id.estimatedDuration, theme)
elapsed = TimeDurationControlSet(activity, dialogView, R.id.elapsedDuration, theme)
estimated.setTimeDuration(estimatedSeconds)
elapsed.setTimeDuration(elapsedSeconds)
estimated.setTimeDuration(viewModel.estimatedSeconds!!)
elapsed.setTimeDuration(viewModel.elapsedSeconds!!)
refresh()
}
@ -81,13 +69,6 @@ class TimerControlSet : TaskEditControlFragment() {
callback = activity as TimerControlSetCallback
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(EXTRA_ELAPSED, elapsed.timeDurationInSeconds)
outState.putInt(EXTRA_ESTIMATED, estimated.timeDurationInSeconds)
outState.putLong(EXTRA_STARTED, timerStarted)
}
override fun onRowClick() {
if (dialog == null) {
dialog = buildDialog()
@ -95,9 +76,6 @@ class TimerControlSet : TaskEditControlFragment() {
dialog!!.show()
}
override val isClickable: Boolean
get() = true
private fun buildDialog(): AlertDialog {
return dialogBuilder
.newDialog()
@ -112,34 +90,24 @@ class TimerControlSet : TaskEditControlFragment() {
if (timerActive()) {
val task = callback.stopTimer()
elapsed.setTimeDuration(task.elapsedSeconds)
timerStarted = 0
viewModel.timerStarted = 0
chronometer.stop()
refreshDisplayView()
} else {
val task = callback.startTimer()
timerStarted = task.timerStart
viewModel.timerStarted = task.timerStart
chronometer.start()
}
updateChronometer()
}
override val layout: Int
get() = R.layout.control_set_timers
override val layout = R.layout.control_set_timers
override val icon: Int
get() = R.drawable.ic_outline_timer_24px
override val icon = R.drawable.ic_outline_timer_24px
override fun controlId() = TAG
override suspend fun hasChanges(original: Task): Boolean {
return (elapsed.timeDurationInSeconds != original.elapsedSeconds
|| estimated.timeDurationInSeconds != original.estimatedSeconds)
}
override suspend fun apply(task: Task) {
task.elapsedSeconds = elapsed.timeDurationInSeconds
task.estimatedSeconds = estimated.timeDurationInSeconds
}
override val isClickable = true
private fun refresh() {
refreshDisplayView()
@ -148,14 +116,18 @@ class TimerControlSet : TaskEditControlFragment() {
private fun refreshDisplayView() {
var est: String? = null
val estimatedSeconds = estimated.timeDurationInSeconds
if (estimatedSeconds > 0) {
est = getString(R.string.TEA_timer_est, DateUtils.formatElapsedTime(estimatedSeconds.toLong()))
viewModel.estimatedSeconds = estimated.timeDurationInSeconds
if (viewModel.estimatedSeconds!! > 0) {
est = getString(
R.string.TEA_timer_est,
DateUtils.formatElapsedTime(viewModel.estimatedSeconds!!.toLong()))
}
var elap: String? = null
val elapsedSeconds = elapsed.timeDurationInSeconds
if (elapsedSeconds > 0) {
elap = getString(R.string.TEA_timer_elap, DateUtils.formatElapsedTime(elapsedSeconds.toLong()))
viewModel.elapsedSeconds = elapsed.timeDurationInSeconds
if (viewModel.elapsedSeconds!! > 0) {
elap = getString(
R.string.TEA_timer_elap,
DateUtils.formatElapsedTime(viewModel.elapsedSeconds!!.toLong()))
}
val toDisplay: String?
toDisplay = if (!isNullOrEmpty(est) && !isNullOrEmpty(elap)) {
@ -176,7 +148,7 @@ class TimerControlSet : TaskEditControlFragment() {
var elapsed = elapsed.timeDurationInSeconds * 1000L
if (timerActive()) {
chronometer.visibility = View.VISIBLE
elapsed += DateUtilities.now() - timerStarted
elapsed += DateUtilities.now() - viewModel.timerStarted
chronometer.base = SystemClock.elapsedRealtime() - elapsed
if (elapsed > DateUtilities.ONE_DAY) {
chronometer.onChronometerTickListener = OnChronometerTickListener { cArg: Chronometer ->
@ -191,9 +163,7 @@ class TimerControlSet : TaskEditControlFragment() {
}
}
private fun timerActive(): Boolean {
return timerStarted > 0
}
private fun timerActive() = viewModel.timerStarted > 0
interface TimerControlSetCallback {
fun stopTimer(): Task
@ -202,8 +172,5 @@ class TimerControlSet : TaskEditControlFragment() {
companion object {
const val TAG = R.string.TEA_ctrl_timer_pref
private const val EXTRA_STARTED = "extra_started"
private const val EXTRA_ESTIMATED = "extra_estimated"
private const val EXTRA_ELAPSED = "extra_elapsed"
}
}

@ -67,9 +67,6 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
spinner.performClick()
}
override val isClickable: Boolean
get() = true
override fun createView(savedInstanceState: Bundle?) {
adapter = object : HiddenTopArrayAdapter<HideUntilValue>(
activity, android.R.layout.simple_spinner_item, spinnerItems) {
@ -94,8 +91,8 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
}
}
if (savedInstanceState == null) {
val dueDate = task.dueDate
var hideUntil = task.hideUntil
val dueDate = viewModel.dueDate!!
var hideUntil = viewModel.hideUntil!!
val dueDay = DateTimeUtils.newDateTime(dueDate)
.withHourOfDay(0)
.withMinuteOfHour(0)
@ -107,7 +104,7 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
if (hideUntil <= 0) {
selection = 0
hideUntil = 0
if (task.isNew) {
if (viewModel.isNew) {
when (preferences.getIntegerFromString(R.string.p_default_hideUntil_key, Task.HIDE_UNTIL_NONE)) {
Task.HIDE_UNTIL_DUE -> selection = 1
Task.HIDE_UNTIL_DAY_BEFORE -> selection = 3
@ -138,15 +135,13 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
refreshDisplayView()
}
override val layout: Int
get() = R.layout.control_set_hide
override val layout = R.layout.control_set_hide
override val icon: Int
get() = R.drawable.ic_outline_visibility_off_24px
override val icon = R.drawable.ic_outline_visibility_off_24px
override fun controlId(): Int {
return TAG
}
override fun controlId() = TAG
override val isClickable = true
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_HIDE_UNTIL) {
@ -158,18 +153,6 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
}
}
override suspend fun apply(task: Task) {
task.hideUntil = getHideUntil(task)
}
override suspend fun hasChanges(original: Task): Boolean {
return original.hideUntil != getHideUntil(original)
}
private fun getHideUntil(task: Task): Long {
return task.createHideUntil(selectedValue!!.setting, selectedValue!!.date)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(EXTRA_CUSTOM, existingDate)
@ -251,6 +234,8 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
private fun refreshDisplayView() {
selectedValue = adapter.getItem(selection)
viewModel.hideUntil = viewModel.task
?.createHideUntil(selectedValue!!.setting, selectedValue!!.date)
clearButton.visibility = if (selectedValue!!.setting == Task.HIDE_UNTIL_NONE) View.GONE else View.VISIBLE
}

@ -13,24 +13,23 @@ import android.widget.ArrayAdapter
import android.widget.Spinner
import com.todoroo.andlib.utility.DateUtilities
import org.tasks.R
import org.tasks.ui.TaskEditViewModel
/**
* Control set dealing with random reminder settings
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
internal class RandomReminderControlSet(context: Context, parentView: View, reminderPeriod: Long) {
private val hours: IntArray
private var selectedIndex = 0
val reminderPeriod: Long
get() {
val hourValue = hours[selectedIndex]
return hourValue * DateUtilities.ONE_HOUR
}
internal class RandomReminderControlSet(context: Context, parentView: View, reminderPeriod: Long, vm: TaskEditViewModel) {
init {
val periodSpinner = parentView.findViewById<Spinner>(R.id.reminder_random_interval)
periodSpinner.visibility = View.VISIBLE
// create hour array
val hourStrings = context.resources.getStringArray(R.array.TEA_reminder_random_hours)
val hours = IntArray(hourStrings.size)
for (i in hours.indices) {
hours[i] = hourStrings[i].toInt()
}
// create adapter
val adapter = ArrayAdapter(
context,
@ -39,19 +38,13 @@ internal class RandomReminderControlSet(context: Context, parentView: View, remi
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
periodSpinner.adapter = adapter
periodSpinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
selectedIndex = position
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
vm.reminderPeriod = hours[position] * DateUtilities.ONE_HOUR
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
// create hour array
val hourStrings = context.resources.getStringArray(R.array.TEA_reminder_random_hours)
hours = IntArray(hourStrings.size)
for (i in hours.indices) {
hours[i] = hourStrings[i].toInt()
}
var i = 0
while (i < hours.size - 1) {
if (hours[i] * DateUtilities.ONE_HOUR >= reminderPeriod) {

@ -14,17 +14,13 @@ import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.activities.DateAndTimePickerActivity
import org.tasks.data.Alarm
import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.MyTimePickerDialog
@ -41,8 +37,6 @@ import javax.inject.Inject
*/
@AndroidEntryPoint
class ReminderControlSet : TaskEditControlFragment() {
private val alarms: MutableSet<Long> = LinkedHashSet()
@Inject lateinit var activity: Activity
@Inject lateinit var alarmService: AlarmService
@Inject lateinit var locale: Locale
@ -54,38 +48,35 @@ class ReminderControlSet : TaskEditControlFragment() {
@BindView(R.id.reminder_alarm)
lateinit var mode: TextView
private var taskId: Long = 0
private var flags = 0
private var randomReminder: Long = 0
private var ringMode = 0
private var randomControlSet: RandomReminderControlSet? = null
private var whenDue = false
private var whenOverdue = false
override fun createView(savedInstanceState: Bundle?) {
mode.paintFlags = mode.paintFlags or Paint.UNDERLINE_TEXT_FLAG
taskId = task.id
if (savedInstanceState == null) {
flags = task.reminderFlags
randomReminder = task.reminderPeriod
} else {
flags = savedInstanceState.getInt(EXTRA_FLAGS)
randomReminder = savedInstanceState.getLong(EXTRA_RANDOM_REMINDER)
when {
viewModel.ringNonstop!! -> setRingMode(2)
viewModel.ringFiveTimes!! -> setRingMode(1)
else -> setRingMode(0)
}
setup(savedInstanceState)
}
private suspend fun currentAlarms(): List<Long> {
return if (taskId == Task.NO_ID) {
emptyList()
} else {
alarmService.getAlarms(taskId).map(Alarm::time)
if (viewModel.whenDue!!) {
addDue()
}
if (viewModel.whenOverdue!!) {
addOverdue()
}
if (viewModel.reminderPeriod!! > 0) {
addRandomReminder(viewModel.reminderPeriod!!)
}
viewModel.selectedAlarms?.forEach(this::addAlarmRow)
}
@OnClick(R.id.reminder_alarm)
fun onClickRingType() {
val modes = resources.getStringArray(R.array.reminder_ring_modes)
val ringMode = when {
viewModel.ringNonstop == true -> 2
viewModel.ringFiveTimes == true -> 1
else -> 0
}
dialogBuilder
.newDialog()
.setSingleChoiceItems(modes, ringMode) { dialog: DialogInterface, which: Int ->
@ -96,7 +87,8 @@ class ReminderControlSet : TaskEditControlFragment() {
}
private fun setRingMode(ringMode: Int) {
this.ringMode = ringMode
viewModel.ringNonstop = ringMode == 2
viewModel.ringFiveTimes = ringMode == 1
mode.setText(getRingModeString(ringMode))
}
@ -126,9 +118,7 @@ class ReminderControlSet : TaskEditControlFragment() {
} else {
dialogBuilder
.newDialog()
.setItems(
options
) { dialog: DialogInterface, which: Int ->
.setItems(options) { dialog: DialogInterface, which: Int ->
addAlarm(options[which])
dialog.dismiss()
}
@ -136,62 +126,19 @@ class ReminderControlSet : TaskEditControlFragment() {
}
}
override val layout: Int
get() = R.layout.control_set_reminders
override val icon: Int
get() = R.drawable.ic_outline_notifications_24px
override fun controlId(): Int {
return TAG
}
private fun setup(savedInstanceState: Bundle?) {
setValue(flags)
alertContainer.removeAllViews()
if (whenDue) {
addDue()
}
if (whenOverdue) {
addOverdue()
}
if (randomReminder > 0) {
addRandomReminder(randomReminder)
}
if (savedInstanceState == null) {
lifecycleScope.launch {
currentAlarms().forEach { addAlarmRow(it) }
}
} else {
savedInstanceState.getLongArray(EXTRA_ALARMS)?.forEach { addAlarmRow(it) }
}
}
override suspend fun hasChanges(original: Task): Boolean {
return getFlags() != original.reminderFlags || randomReminderPeriod != original.reminderPeriod || HashSet(currentAlarms()) != alarms
}
override val layout = R.layout.control_set_reminders
override fun requiresId() = true
override val icon = R.drawable.ic_outline_notifications_24px
override suspend fun apply(task: Task) {
task.reminderFlags = getFlags()
task.reminderPeriod = randomReminderPeriod
if (alarmService.synchronizeAlarms(task.id, alarms)) {
task.modificationDate = DateUtilities.now()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(EXTRA_FLAGS, getFlags())
outState.putLong(EXTRA_RANDOM_REMINDER, randomReminderPeriod)
outState.putLongArray(EXTRA_ALARMS, alarms.toLongArray())
}
override fun controlId() = TAG
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_NEW_ALARM) {
if (resultCode == Activity.RESULT_OK) {
addAlarmRow(data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L))
val timestamp = data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L)
if (viewModel.selectedAlarms?.add(timestamp) == true) {
addAlarmRow(timestamp)
}
}
} else {
super.onActivityResult(requestCode, resultCode, data)
@ -199,31 +146,11 @@ class ReminderControlSet : TaskEditControlFragment() {
}
private fun addAlarmRow(timestamp: Long) {
if (alarms.add(timestamp)) {
addAlarmRow(DateUtilities.getLongDateStringWithTime(timestamp, locale.locale), View.OnClickListener { alarms.remove(timestamp) })
}
addAlarmRow(
DateUtilities.getLongDateStringWithTime(timestamp, locale.locale),
View.OnClickListener { viewModel.selectedAlarms?.remove(timestamp) })
}
private fun getFlags(): Int {
var value = 0
if (whenDue) {
value = value or Task.NOTIFY_AT_DEADLINE
}
if (whenOverdue) {
value = value or Task.NOTIFY_AFTER_DEADLINE
}
value = value and (Task.NOTIFY_MODE_FIVE or Task.NOTIFY_MODE_NONSTOP).inv()
if (ringMode == 2) {
value = value or Task.NOTIFY_MODE_NONSTOP
} else if (ringMode == 1) {
value = value or Task.NOTIFY_MODE_FIVE
}
return value
}
private val randomReminderPeriod: Long
get() = if (randomControlSet == null) 0L else randomControlSet!!.reminderPeriod
private fun addNewAlarm() {
val intent = Intent(activity, DateAndTimePickerActivity::class.java)
intent.putExtra(
@ -252,10 +179,10 @@ class ReminderControlSet : TaskEditControlFragment() {
private val options: List<String>
get() {
val options: MutableList<String> = ArrayList()
if (!whenDue) {
if (viewModel.whenDue != true) {
options.add(getString(R.string.when_due))
}
if (!whenOverdue) {
if (viewModel.whenOverdue != true) {
options.add(getString(R.string.when_overdue))
}
if (randomControlSet == null) {
@ -266,35 +193,29 @@ class ReminderControlSet : TaskEditControlFragment() {
}
private fun addDue() {
whenDue = true
addAlarmRow(getString(R.string.when_due), View.OnClickListener { whenDue = false })
viewModel.whenDue = true
addAlarmRow(getString(R.string.when_due), View.OnClickListener {
viewModel.whenDue = false
})
}
private fun addOverdue() {
whenOverdue = true
addAlarmRow(getString(R.string.when_overdue), View.OnClickListener { whenOverdue = false })
viewModel.whenOverdue = true
addAlarmRow(getString(R.string.when_overdue), View.OnClickListener {
viewModel.whenOverdue = false
})
}
private fun addRandomReminder(reminderPeriod: Long) {
val alarmRow = addAlarmRow(getString(R.string.randomly_once) + " ", View.OnClickListener { randomControlSet = null })
randomControlSet = RandomReminderControlSet(activity, alarmRow, reminderPeriod)
}
private fun setValue(flags: Int) {
whenDue = flags and Task.NOTIFY_AT_DEADLINE > 0
whenOverdue = flags and Task.NOTIFY_AFTER_DEADLINE > 0
when {
flags and Task.NOTIFY_MODE_NONSTOP > 0 -> setRingMode(2)
flags and Task.NOTIFY_MODE_FIVE > 0 -> setRingMode(1)
else -> setRingMode(0)
}
val alarmRow = addAlarmRow(getString(R.string.randomly_once) + " ", View.OnClickListener {
viewModel.reminderPeriod = 0
randomControlSet = null
})
randomControlSet = RandomReminderControlSet(activity, alarmRow, reminderPeriod, viewModel)
}
companion object {
const val TAG = R.string.TEA_ctrl_reminders_pref
private const val REQUEST_NEW_ALARM = 12152
private const val EXTRA_FLAGS = "extra_flags"
private const val EXTRA_RANDOM_REMINDER = "extra_random_reminder"
private const val EXTRA_ALARMS = "extra_alarms"
}
}

@ -16,4 +16,8 @@ public class Event<T> {
handled = true;
return value;
}
public T getValue() {
return value;
}
}

@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import com.todoroo.astrid.data.Task
@Dao
interface AlarmDao {
@ -28,4 +29,10 @@ interface AlarmDao {
@Insert
suspend fun insert(alarms: Iterable<Alarm>)
suspend fun getAlarms(task: Task) = ArrayList(if (task.isNew) {
emptyList()
} else {
getAlarms(task.id)
})
}

@ -83,16 +83,12 @@ class CommentBarFragment : TaskEditControlFragment() {
resetPictureButton()
}
override val layout: Int
get() = R.layout.fragment_comment_bar
override val layout = R.layout.fragment_comment_bar
override val icon: Int
get() = 0
override val icon = 0
override fun controlId() = TAG
override suspend fun apply(task: Task) {}
@OnTextChanged(R.id.commentField)
fun onTextChanged(s: CharSequence) {
commentButton.visibility = if (pendingCommentPicture == null && isNullOrEmpty(s.toString())) View.GONE else View.VISIBLE

@ -1,10 +1,8 @@
package org.tasks.fragments
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import com.todoroo.astrid.activity.BeastModePreferences
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.files.FilesControlSet
import com.todoroo.astrid.repeats.RepeatControlSet
import com.todoroo.astrid.tags.TagsControlSet
@ -27,17 +25,7 @@ class TaskEditControlSetFragmentManager @Inject constructor(
private val displayOrder: List<String>
var visibleSize = 0
fun getFragmentsInPersistOrder(fragmentManager: FragmentManager): List<TaskEditControlFragment> {
return controlSetFragments.keys
.mapNotNull { fragmentManager.findFragmentByTag(it) as TaskEditControlFragment? }
}
fun getOrCreateFragments(
fragmentManager: FragmentManager,
task: Task,
arguments: Bundle): List<TaskEditControlFragment> {
arguments.putParcelable(TaskEditControlFragment.EXTRA_TASK, task)
arguments.putBoolean(TaskEditControlFragment.EXTRA_IS_NEW, task.isNew)
fun getOrCreateFragments(fragmentManager: FragmentManager): List<TaskEditControlFragment> {
val fragments: MutableList<TaskEditControlFragment> = ArrayList()
for (i in displayOrder.indices) {
val tag = displayOrder[i]
@ -45,7 +33,6 @@ class TaskEditControlSetFragmentManager @Inject constructor(
if (fragment == null) {
val resId = controlSetFragments[tag]
fragment = createFragment(resId!!)
fragment.arguments = arguments
}
fragments.add(fragment)
}

@ -1,19 +1,14 @@
package org.tasks.ui
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.CalendarContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.gcal.GCalHelper
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.PermissionUtil.verifyPermissions
@ -50,108 +45,33 @@ class CalendarControlSet : TaskEditControlFragment() {
@Inject lateinit var themeBase: ThemeBase
@Inject lateinit var calendarEventProvider: CalendarEventProvider
private var calendarId: String? = null
private var eventUri: String? = null
override fun onResume() {
super.onResume()
override fun createView(savedInstanceState: Bundle?) {
val canAccessCalendars = permissionChecker.canAccessCalendars()
if (savedInstanceState != null) {
eventUri = savedInstanceState.getString(EXTRA_URI)
calendarId = savedInstanceState.getString(EXTRA_ID)
} else if (task.isNew && canAccessCalendars) {
calendarId = preferences.defaultCalendar
if (!isNullOrEmpty(calendarId)) {
try {
val defaultCalendar = calendarProvider.getCalendar(calendarId)
if (defaultCalendar == null) {
calendarId = null
}
} catch (e: Exception) {
Timber.e(e)
firebase.reportException(e)
calendarId = null
}
viewModel.eventUri?.let {
if (canAccessCalendars && !calendarEntryExists(it)) {
viewModel.eventUri = null
}
} else {
eventUri = task.calendarURI
}
if (canAccessCalendars && !calendarEntryExists(eventUri)) {
eventUri = null
if (!canAccessCalendars) {
viewModel.selectedCalendar = null
}
refreshDisplayView()
}
override val layout: Int
get() = R.layout.control_set_gcal_display
override val layout = R.layout.control_set_gcal_display
override val icon: Int
get() = R.drawable.ic_outline_event_24px
override val icon = R.drawable.ic_outline_event_24px
override fun controlId() = TAG
override suspend fun hasChanges(original: Task): Boolean {
if (!permissionChecker.canAccessCalendars()) {
return false
}
if (!isNullOrEmpty(calendarId)) {
return true
}
val originalUri = original.calendarURI
return if (isNullOrEmpty(eventUri) && isNullOrEmpty(originalUri)) {
false
} else originalUri != eventUri
}
override suspend fun apply(task: Task) {
if (!permissionChecker.canAccessCalendars()) {
return
}
if (!isNullOrEmpty(task.calendarURI)) {
if (eventUri == null) {
calendarEventProvider.deleteEvent(task)
} else if (!calendarEntryExists(task.calendarURI)) {
task.calendarURI = ""
}
}
if (!task.hasDueDate()) {
return
}
if (calendarEntryExists(task.calendarURI)) {
val cr = activity.contentResolver
try {
val updateValues = ContentValues()
// check if we need to update the item
updateValues.put(CalendarContract.Events.TITLE, task.title)
updateValues.put(CalendarContract.Events.DESCRIPTION, task.notes)
gcalHelper.createStartAndEndDate(task, updateValues)
cr.update(Uri.parse(task.calendarURI), updateValues, null, null)
} catch (e: Exception) {
Timber.e(e, "unable-to-update-calendar: %s", task.calendarURI)
}
} else if (!isNullOrEmpty(calendarId)) {
try {
val values = ContentValues()
values.put(CalendarContract.Events.CALENDAR_ID, calendarId)
val uri = gcalHelper.createTaskEvent(task, values)
if (uri != null) {
task.calendarURI = uri.toString()
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_URI, eventUri)
outState.putString(EXTRA_ID, calendarId)
}
override val isClickable = true
@OnClick(R.id.clear)
fun clearCalendar() {
if (isNullOrEmpty(eventUri)) {
if (viewModel.eventUri.isNullOrBlank()) {
clear()
} else {
dialogBuilder
@ -167,13 +87,13 @@ class CalendarControlSet : TaskEditControlFragment() {
}
private fun clear() {
calendarId = null
eventUri = null
viewModel.selectedCalendar = null
viewModel.eventUri = null
refreshDisplayView()
}
override fun onRowClick() {
if (isNullOrEmpty(eventUri)) {
if (viewModel.eventUri.isNullOrBlank()) {
CalendarPicker.newCalendarPicker(this, REQUEST_CODE_PICK_CALENDAR, calendarName)
.show(parentFragmentManager, FRAG_TAG_CALENDAR_PICKER)
} else {
@ -183,12 +103,9 @@ class CalendarControlSet : TaskEditControlFragment() {
}
}
override val isClickable: Boolean
get() = true
private fun openCalendarEvent() {
val cr = activity.contentResolver
val uri = Uri.parse(eventUri)
val uri = Uri.parse(viewModel.eventUri)
val intent = Intent(Intent.ACTION_VIEW, uri)
try {
cr.query(
@ -199,7 +116,7 @@ class CalendarControlSet : TaskEditControlFragment() {
if (cursor!!.count == 0) {
// event no longer exists
Toast.makeText(activity, R.string.calendar_event_not_found, Toast.LENGTH_SHORT).show()
eventUri = null
viewModel.eventUri = null
refreshDisplayView()
} else {
cursor.moveToFirst()
@ -217,7 +134,7 @@ class CalendarControlSet : TaskEditControlFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PICK_CALENDAR) {
if (resultCode == Activity.RESULT_OK) {
calendarId = data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID)
viewModel.selectedCalendar = data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID)
refreshDisplayView()
}
} else {
@ -226,13 +143,7 @@ class CalendarControlSet : TaskEditControlFragment() {
}
private val calendarName: String?
get() {
if (calendarId == null) {
return null
}
val calendar = calendarProvider.getCalendar(calendarId)
return calendar?.name
}
get() = viewModel.selectedCalendar?.let { calendarProvider.getCalendar(it)?.name }
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
@ -249,14 +160,16 @@ class CalendarControlSet : TaskEditControlFragment() {
}
}
private fun refreshDisplayView() {
if (!isNullOrEmpty(eventUri)) {
private fun refreshDisplayView() = when {
viewModel.eventUri?.isNotBlank() == true -> {
calendar.setText(R.string.gcal_TEA_showCalendar_label)
cancelButton.visibility = View.VISIBLE
} else if (calendarId != null) {
}
!viewModel.selectedCalendar.isNullOrBlank() -> {
calendar.text = calendarName
cancelButton.visibility = View.GONE
} else {
}
else -> {
calendar.text = null
cancelButton.visibility = View.GONE
}
@ -287,7 +200,5 @@ class CalendarControlSet : TaskEditControlFragment() {
private const val REQUEST_CODE_PICK_CALENDAR = 70
private const val REQUEST_CODE_OPEN_EVENT = 71
private const val REQUEST_CODE_CLEAR_EVENT = 72
private const val EXTRA_URI = "extra_uri"
private const val EXTRA_ID = "extra_id"
}
}

@ -6,8 +6,6 @@ import android.os.Bundle
import android.widget.TextView
import butterknife.BindView
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.createDueDate
import com.todoroo.astrid.data.Task.Companion.hasDueTime
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
@ -16,7 +14,6 @@ import org.tasks.dialogs.DateTimePicker
import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.locale.Locale
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import java.time.format.FormatStyle
import javax.inject.Inject
@ -30,7 +27,6 @@ class DeadlineControlSet : TaskEditControlFragment() {
lateinit var dueDate: TextView
private lateinit var callback: DueDateChangeListener
private var date: Long = 0
override fun onAttach(activity: Activity) {
super.onAttach(activity)
@ -38,7 +34,6 @@ class DeadlineControlSet : TaskEditControlFragment() {
}
override fun createView(savedInstanceState: Bundle?) {
date = savedInstanceState?.getLong(EXTRA_DATE) ?: task.dueDate
refreshDisplayView()
}
@ -48,42 +43,25 @@ class DeadlineControlSet : TaskEditControlFragment() {
newDateTimePicker(
this,
REQUEST_DATE,
dueDateTime,
viewModel.dueDate!!,
preferences.getBoolean(R.string.p_auto_dismiss_datetime_edit_screen, false))
.show(fragmentManager, FRAG_TAG_DATE_PICKER)
}
}
override val isClickable: Boolean
get() = true
override val isClickable = true
override val layout: Int
get() = R.layout.control_set_deadline
override val layout = R.layout.control_set_deadline
override val icon: Int
get() = R.drawable.ic_outline_schedule_24px
override val icon = R.drawable.ic_outline_schedule_24px
override fun controlId() = TAG
override suspend fun hasChanges(original: Task): Boolean {
return original.dueDate != dueDateTime
}
override suspend fun apply(task: Task) {
val dueDate = dueDateTime
if (dueDate != task.dueDate) {
task.reminderSnooze = 0L
}
task.dueDate = dueDate
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_DATE) {
if (resultCode == Activity.RESULT_OK) {
val timestamp = data!!.getLongExtra(DateTimePicker.EXTRA_TIMESTAMP, 0L)
val dateTime = DateTime(timestamp)
date = dateTime.millis
callback.dueDateChanged(dueDateTime)
viewModel.dueDate = data!!.getLongExtra(DateTimePicker.EXTRA_TIMESTAMP, 0L)
callback.dueDateChanged()
}
refreshDisplayView()
} else {
@ -91,24 +69,18 @@ class DeadlineControlSet : TaskEditControlFragment() {
}
}
private val dueDateTime: Long
get() = if (date == 0L) 0 else createDueDate(
if (hasDueTime(date)) Task.URGENCY_SPECIFIC_DAY_TIME else Task.URGENCY_SPECIFIC_DAY,
date)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(EXTRA_DATE, date)
}
private fun refreshDisplayView() {
val date = viewModel.dueDate!!
if (date == 0L) {
dueDate.text = ""
setTextColor(false)
} else {
dueDate.text = DateUtilities.getRelativeDateTime(activity, date, locale.locale, FormatStyle.FULL)
setTextColor(
if (hasDueTime(date)) DateTimeUtils.newDateTime(date).isBeforeNow else DateTimeUtils.newDateTime(date).endOfDay().isBeforeNow)
setTextColor(if (hasDueTime(date)) {
DateTimeUtils.newDateTime(date).isBeforeNow
} else {
DateTimeUtils.newDateTime(date).endOfDay().isBeforeNow
})
}
}
@ -118,13 +90,12 @@ class DeadlineControlSet : TaskEditControlFragment() {
}
interface DueDateChangeListener {
fun dueDateChanged(dateTime: Long)
fun dueDateChanged()
}
companion object {
const val TAG = R.string.TEA_ctrl_when_pref
private const val REQUEST_DATE = 504
private const val EXTRA_DATE = "extra_date"
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
}
}

@ -4,10 +4,8 @@ import android.os.Bundle
import android.widget.EditText
import butterknife.BindView
import butterknife.OnTextChanged
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.dialogs.Linkify
import org.tasks.preferences.Preferences
import javax.inject.Inject
@ -20,53 +18,25 @@ class DescriptionControlSet : TaskEditControlFragment() {
@BindView(R.id.notes)
lateinit var editText: EditText
private var description: String? = null
override fun createView(savedInstanceState: Bundle?) {
description = if (savedInstanceState == null) {
stripCarriageReturns(task.notes)
} else {
savedInstanceState.getString(EXTRA_DESCRIPTION)
}
if (!isNullOrEmpty(description)) {
editText.setTextKeepState(description)
}
viewModel.description?.let(editText::setTextKeepState)
if (preferences.getBoolean(R.string.p_linkify_task_edit, false)) {
linkify.linkify(editText)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_DESCRIPTION, description)
}
override val layout: Int
get() = R.layout.control_set_description
override val layout = R.layout.control_set_description
override val icon: Int
get() = R.drawable.ic_outline_notes_24px
override val icon = R.drawable.ic_outline_notes_24px
override fun controlId() = TAG
@OnTextChanged(R.id.notes)
fun textChanged(text: CharSequence) {
description = text.toString().trim { it <= ' ' }
}
override suspend fun apply(task: Task) {
task.notes = description
}
override suspend fun hasChanges(original: Task): Boolean {
return !if (isNullOrEmpty(description)) isNullOrEmpty(original.notes) else description == stripCarriageReturns(original.notes)
viewModel.description = text.toString().trim { it <= ' ' }
}
companion object {
const val TAG = R.string.TEA_ctrl_notes_pref
private const val EXTRA_DESCRIPTION = "extra_description"
fun stripCarriageReturns(original: String?): String? {
return original?.replace("\\r\\n?".toRegex(), "\n")
}
}
}

@ -5,11 +5,9 @@ import android.content.Intent
import android.os.Bundle
import butterknife.BindView
import com.google.android.material.chip.ChipGroup
import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
@ -21,11 +19,8 @@ class ListFragment : TaskEditControlFragment() {
@BindView(R.id.chip_group)
lateinit var chipGroup: ChipGroup
@Inject lateinit var taskMover: TaskMover
@Inject lateinit var chipProvider: ChipProvider
private var originalList: Filter? = null
private lateinit var selectedList: Filter
private lateinit var callback: OnListChanged
interface OnListChanged {
@ -38,62 +33,38 @@ class ListFragment : TaskEditControlFragment() {
}
override fun createView(savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
originalList = requireArguments().getParcelable(TaskEditFragment.EXTRA_LIST)!!
setSelected(originalList!!)
} else {
originalList = savedInstanceState.getParcelable(EXTRA_ORIGINAL_LIST)
setSelected(savedInstanceState.getParcelable(EXTRA_SELECTED_LIST)!!)
}
refreshView()
}
private fun setSelected(filter: Filter) {
selectedList = filter
viewModel.selectedList = filter
refreshView()
callback.onListChanged(filter)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_ORIGINAL_LIST, originalList)
outState.putParcelable(EXTRA_SELECTED_LIST, selectedList)
}
override val layout: Int
get() = R.layout.control_set_remote_list
override val layout = R.layout.control_set_remote_list
override val icon: Int
get() = R.drawable.ic_list_24px
override val icon = R.drawable.ic_list_24px
override fun controlId() = TAG
override fun onRowClick() = openPicker()
override val isClickable: Boolean
get() = true
override val isClickable = true
private fun openPicker() =
ListPicker.newListPicker(selectedList, this, REQUEST_CODE_SELECT_LIST)
ListPicker.newListPicker(viewModel.selectedList!!, this, REQUEST_CODE_SELECT_LIST)
.show(parentFragmentManager, FRAG_TAG_GOOGLE_TASK_LIST_SELECTION)
override fun requiresId() = true
override suspend fun apply(task: Task) {
if (isNew || hasChanges()) {
task.parent = 0
taskMover.move(listOf(task.id), selectedList)
}
}
override suspend fun hasChanges(original: Task) = hasChanges()
private fun hasChanges() = selectedList != originalList
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SELECT_LIST) {
if (resultCode == Activity.RESULT_OK) {
data?.getParcelableExtra<Filter>(ListPicker.EXTRA_SELECTED_FILTER)?.let {
setList(it)
if (it is GtasksFilter || it is CaldavFilter) {
setSelected(it)
} else {
throw RuntimeException("Unhandled filter type")
}
}
}
} else {
@ -101,17 +72,13 @@ class ListFragment : TaskEditControlFragment() {
}
}
private fun setList(list: Filter) {
if (list is GtasksFilter || list is CaldavFilter) {
setSelected(list)
} else {
throw RuntimeException("Unhandled filter type")
}
}
private fun refreshView() {
chipGroup.removeAllViews()
val chip = chipProvider.newChip(selectedList, R.drawable.ic_list_24px, showText = true, showIcon = true)!!
val chip = chipProvider.newChip(
viewModel.selectedList!!,
R.drawable.ic_list_24px,
showText = true,
showIcon = true)!!
chip.setOnClickListener { openPicker() }
chipGroup.addView(chip)
}
@ -119,8 +86,6 @@ class ListFragment : TaskEditControlFragment() {
companion object {
const val TAG = R.string.TEA_ctrl_google_task_list
private const val FRAG_TAG_GOOGLE_TASK_LIST_SELECTION = "frag_tag_google_task_list_selection"
private const val EXTRA_ORIGINAL_LIST = "extra_original_list"
private const val EXTRA_SELECTED_LIST = "extra_selected_list"
private const val REQUEST_CODE_SELECT_LIST = 10101
}
}

@ -14,21 +14,15 @@ import android.widget.TextView
import androidx.core.util.Pair
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.data.SyncFlags
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.PermissionUtil.verifyPermissions
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.Geofence
import org.tasks.data.Location
import org.tasks.data.LocationDao
import org.tasks.data.Place
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.GeofenceDialog
import org.tasks.location.GeofenceApi
import org.tasks.location.LocationPickerActivity
import org.tasks.preferences.*
import java.util.*
@ -38,8 +32,6 @@ import javax.inject.Inject
class LocationControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var geofenceApi: GeofenceApi
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var device: Device
@Inject lateinit var permissionRequestor: FragmentPermissionRequestor
@Inject lateinit var permissionChecker: PermissionChecker
@ -53,32 +45,19 @@ class LocationControlSet : TaskEditControlFragment() {
@BindView(R.id.geofence_options)
lateinit var geofenceOptions: ImageView
private var original: Location? = null
private var location: Location? = null
override fun createView(savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
original = requireArguments().getParcelable(TaskEditFragment.EXTRA_PLACE)
if (original != null) {
setLocation(Location(original!!.geofence, original!!.place))
}
} else {
original = savedInstanceState.getParcelable(EXTRA_ORIGINAL)
location = savedInstanceState.getParcelable(EXTRA_LOCATION)
}
}
override fun onResume() {
super.onResume()
updateUi()
}
private fun setLocation(location: Location?) {
this.location = location
viewModel.selectedLocation = location
updateUi()
}
private fun updateUi() {
val location = viewModel.selectedLocation
if (location == null) {
locationName.text = ""
geofenceOptions.visibility = View.GONE
@ -87,9 +66,9 @@ class LocationControlSet : TaskEditControlFragment() {
geofenceOptions.visibility = if (device.supportsGeofences()) View.VISIBLE else View.GONE
geofenceOptions.setImageResource(
if (permissionChecker.canAccessLocation()
&& (location!!.isArrival || location!!.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px)
val name = location!!.displayName
val address = location!!.displayAddress
&& (location.isArrival || location.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px)
val name = location.displayName
val address = location.displayAddress
if (!isNullOrEmpty(address) && address != name) {
locationAddress.text = address
locationAddress.visibility = View.VISIBLE
@ -109,22 +88,23 @@ class LocationControlSet : TaskEditControlFragment() {
}
override fun onRowClick() {
val location = viewModel.selectedLocation
if (location == null) {
chooseLocation()
} else {
val options: MutableList<Pair<Int, () -> Unit>> = ArrayList()
options.add(Pair.create(R.string.open_map, { location!!.open(activity) }))
if (!isNullOrEmpty(location!!.phone)) {
options.add(Pair.create(R.string.open_map, { location.open(activity) }))
if (!isNullOrEmpty(location.phone)) {
options.add(Pair.create(R.string.action_call, { call() }))
}
if (!isNullOrEmpty(location!!.url)) {
if (!isNullOrEmpty(location.url)) {
options.add(Pair.create(R.string.visit_website, { openWebsite() }))
}
options.add(Pair.create(R.string.choose_new_location, { chooseLocation() }))
options.add(Pair.create(R.string.delete, { setLocation(null) }))
val items = options.map { requireContext().getString(it.first!!) }
dialogBuilder
.newDialog(location!!.displayName)
.newDialog(location.displayName)
.setItems(items) { _, which: Int ->
options[which].second!!.invoke()
}
@ -132,13 +112,10 @@ class LocationControlSet : TaskEditControlFragment() {
}
}
override val isClickable: Boolean
get() = true
private fun chooseLocation() {
val intent = Intent(activity, LocationPickerActivity::class.java)
if (location != null) {
intent.putExtra(LocationPickerActivity.EXTRA_PLACE, location!!.place as Parcelable)
viewModel.selectedLocation?.let {
intent.putExtra(LocationPickerActivity.EXTRA_PLACE, it.place as Parcelable)
}
startActivityForResult(intent, REQUEST_LOCATION_REMINDER)
}
@ -168,78 +145,40 @@ class LocationControlSet : TaskEditControlFragment() {
}
private fun showGeofenceOptions() {
val dialog = GeofenceDialog.newGeofenceDialog(location)
val dialog = GeofenceDialog.newGeofenceDialog(viewModel.selectedLocation)
dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS)
dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG)
}
override val layout: Int
get() = R.layout.location_row
override val layout = R.layout.location_row
override val icon: Int
get() = R.drawable.ic_outline_place_24px
override val icon = R.drawable.ic_outline_place_24px
override fun controlId() = TAG
private fun openWebsite() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(location!!.url)))
}
private fun call() {
startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + location!!.phone)))
}
override val isClickable = true
override suspend fun hasChanges(task: Task): Boolean {
if (original == null) {
return location != null
}
if (location == null) {
return true
}
return if (original!!.place != location!!.place) {
true
} else {
original!!.isDeparture != location!!.isDeparture
|| original!!.isArrival != location!!.isArrival
|| original!!.radius != location!!.radius
private fun openWebsite() {
viewModel.selectedLocation?.let {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url)))
}
}
override fun requiresId() = true
override suspend fun apply(task: Task) {
if (original == null || location == null || original!!.place != location!!.place) {
task.putTransitory(SyncFlags.FORCE_CALDAV_SYNC, true)
}
if (original != null) {
locationDao.delete(original!!.geofence)
geofenceApi.update(original!!.place)
}
if (location != null) {
val place = location!!.place
val geofence = location!!.geofence
geofence.task = task.id
geofence.place = place.uid
geofence.id = locationDao.insert(geofence)
geofenceApi.update(place)
private fun call() {
viewModel.selectedLocation?.let {
startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.phone)))
}
task.modificationDate = DateUtilities.now()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_ORIGINAL, original)
outState.putParcelable(EXTRA_LOCATION, location)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_LOCATION_REMINDER) {
if (resultCode == Activity.RESULT_OK) {
val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!!
val location = viewModel.selectedLocation
val geofence = if (location == null) {
Geofence(place.uid, preferences)
} else {
val existing = location!!.geofence
val existing = location.geofence
Geofence(
place.uid,
existing.isArrival,
@ -250,7 +189,7 @@ class LocationControlSet : TaskEditControlFragment() {
}
} else if (requestCode == REQUEST_GEOFENCE_DETAILS) {
if (resultCode == Activity.RESULT_OK) {
location!!.geofence = data!!.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE)!!
viewModel.selectedLocation?.geofence = data!!.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE)!!
updateUi()
}
} else {
@ -263,7 +202,5 @@ class LocationControlSet : TaskEditControlFragment() {
private const val REQUEST_LOCATION_REMINDER = 12153
private const val REQUEST_GEOFENCE_DETAILS = 12154
private const val FRAG_TAG_LOCATION_DIALOG = "location_dialog"
private const val EXTRA_ORIGINAL = "extra_original_location"
private const val EXTRA_LOCATION = "extra_new_location"
}
}

@ -2,9 +2,6 @@ package org.tasks.ui
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatRadioButton
import butterknife.BindView
import butterknife.OnClick
@ -30,17 +27,13 @@ class PriorityControlSet : TaskEditControlFragment() {
@BindView(R.id.priority_none)
lateinit var priorityNone: AppCompatRadioButton
@Task.Priority
private var priority = 0
@OnClick(R.id.priority_high, R.id.priority_medium, R.id.priority_low, R.id.priority_none)
fun onPriorityChanged() {
priority = getPriority()
viewModel.priority = getPriority()
}
override fun createView(savedInstanceState: Bundle?) {
priority = savedInstanceState?.getInt(EXTRA_PRIORITY) ?: task.priority
when (priority) {
when (viewModel.priority) {
0 -> priorityHigh.isChecked = true
1 -> priorityMedium.isChecked = true
2 -> priorityLow.isChecked = true
@ -52,49 +45,26 @@ class PriorityControlSet : TaskEditControlFragment() {
tintRadioButton(priorityNone, 3)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(EXTRA_PRIORITY, priority)
}
override val layout: Int
get() = R.layout.control_set_priority
override val layout = R.layout.control_set_priority
override val icon: Int
get() = R.drawable.ic_outline_flag_24px
override val icon = R.drawable.ic_outline_flag_24px
override fun controlId() = TAG
override suspend fun apply(task: Task) {
task.priority = priority
}
override suspend fun hasChanges(original: Task): Boolean {
return original.priority != priority
}
private fun tintRadioButton(radioButton: AppCompatRadioButton, priority: Int) {
val color = colorProvider.getPriorityColor(priority, true)
radioButton.buttonTintList = ColorStateList(arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)), intArrayOf(color, color))
}
@Task.Priority
private fun getPriority(): Int {
if (priorityHigh.isChecked) {
return Task.Priority.HIGH
}
if (priorityMedium.isChecked) {
return Task.Priority.MEDIUM
}
return if (priorityLow.isChecked) {
Task.Priority.LOW
} else {
Task.Priority.NONE
}
private fun getPriority() = when {
priorityHigh.isChecked -> Task.Priority.HIGH
priorityMedium.isChecked -> Task.Priority.MEDIUM
priorityLow.isChecked -> Task.Priority.LOW
else -> Task.Priority.NONE
}
companion object {
const val TAG = R.string.TEA_ctrl_importance_pref
private const val EXTRA_PRIORITY = "extra_priority"
}
}

@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.graphics.Paint
import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -13,6 +14,7 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.LinearLayout
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -24,9 +26,8 @@ import butterknife.OnClick
import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Join
import com.todoroo.andlib.sql.QueryTemplate
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
@ -39,19 +40,19 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.*
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.locale.Locale
import org.tasks.tasklist.SubtaskViewHolder
import org.tasks.tasklist.SubtasksRecyclerAdapter
import java.util.*
import javax.inject.Inject
@AndroidEntryPoint
class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks {
@JvmField
@BindView(R.id.recycler_view)
var recyclerView: RecyclerView? = null
lateinit var recyclerView: RecyclerView
@BindView(R.id.new_subtasks)
lateinit var newSubtaskContainer: LinearLayout
@ -68,102 +69,43 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
@Inject lateinit var checkBoxProvider: CheckBoxProvider
@Inject lateinit var chipProvider: ChipProvider
private lateinit var viewModel: TaskListViewModel
private lateinit var listViewModel: TaskListViewModel
private val refreshReceiver = RefreshReceiver()
private var remoteList: Filter? = null
private var googleTask: GoogleTask? = null
private lateinit var recyclerAdapter: SubtasksRecyclerAdapter
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(EXTRA_NEW_SUBTASKS, newSubtasks)
}
override fun createView(savedInstanceState: Bundle?) {
viewModel = ViewModelProvider(this).get(TaskListViewModel::class.java)
if (savedInstanceState != null) {
for (task in savedInstanceState.getParcelableArrayList<Task>(EXTRA_NEW_SUBTASKS)!!) {
addSubtask(task)
}
}
listViewModel = ViewModelProvider(this).get(TaskListViewModel::class.java)
viewModel.newSubtasks.forEach { addSubtask(it) }
recyclerAdapter = SubtasksRecyclerAdapter(activity, chipProvider, checkBoxProvider, this)
if (task.id > 0) {
recyclerAdapter.submitList(viewModel.value)
viewModel.setFilter(Filter("subtasks", getQueryTemplate(task)))
(recyclerView!!.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView!!.layoutManager = LinearLayoutManager(activity)
recyclerView!!.isNestedScrollingEnabled = false
viewModel.observe(this, Observer { list: List<TaskContainer?>? -> recyclerAdapter.submitList(list) })
recyclerView!!.adapter = recyclerAdapter
viewModel.task?.let {
if (it.id > 0) {
recyclerAdapter.submitList(listViewModel.value)
listViewModel.setFilter(Filter("subtasks", getQueryTemplate(it)))
(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView.layoutManager = LinearLayoutManager(activity)
recyclerView.isNestedScrollingEnabled = false
listViewModel.observe(this, Observer { list: List<TaskContainer?>? -> recyclerAdapter.submitList(list) })
recyclerView.adapter = recyclerAdapter
}
}
}
override val layout: Int
get() = R.layout.control_set_subtasks
override val layout = R.layout.control_set_subtasks
override val icon: Int
get() = R.drawable.ic_subdirectory_arrow_right_black_24dp
override val icon = R.drawable.ic_subdirectory_arrow_right_black_24dp
override fun controlId() = TAG
override fun requiresId() = true
override suspend fun apply(task: Task) {
for (subtask in newSubtasks) {
if (isNullOrEmpty(subtask.title)) {
continue
}
subtask.completionDate = task.completionDate
taskDao.createNew(subtask)
when (remoteList) {
is GtasksFilter -> {
val googleTask = GoogleTask(subtask.id, (remoteList as GtasksFilter).remoteId)
googleTask.parent = task.id
googleTask.isMoved = true
googleTaskDao.insertAndShift(googleTask, false)
}
is CaldavFilter -> {
val caldavTask = CaldavTask(subtask.id, (remoteList as CaldavFilter).uuid)
subtask.parent = task.id
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.id)
taskDao.save(subtask)
caldavDao.insert(subtask, caldavTask, false)
}
else -> {
subtask.parent = task.id
taskDao.save(subtask)
}
}
}
}
override suspend fun hasChanges(original: Task): Boolean {
return newSubtasks.isNotEmpty()
}
private val newSubtasks: ArrayList<Task>
get() {
val subtasks = ArrayList<Task>()
val children = newSubtaskContainer.childCount
for (i in 0 until children) {
val view = newSubtaskContainer.getChildAt(i) as ViewGroup
val title = view.getChildAt(2) as EditText
val completed: CheckableImageView = view.findViewById(R.id.completeBox)
val subtask = taskCreator.createWithValues(title.text.toString())
if (completed.isChecked) {
subtask.completionDate = DateUtilities.now()
}
subtasks.add(subtask)
}
return subtasks
}
override fun onResume() {
super.onResume()
localBroadcastManager.registerRefreshReceiver(refreshReceiver)
lifecycleScope.launch {
googleTask = googleTaskDao.getByTaskId(task.id)
updateUI()
viewModel.task?.let {
googleTask = googleTaskDao.getByTaskId(it.id)
updateUI()
}
}
}
@ -178,7 +120,9 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
toaster.longToast(R.string.subtasks_multilevel_google_task)
return
}
val editText = addSubtask(taskCreator.createWithValues(""))
val task = taskCreator.createWithValues("")
viewModel.newSubtasks.add(task)
val editText = addSubtask(task)
editText.requestFocus()
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
@ -195,6 +139,9 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
editText.maxLines = Int.MAX_VALUE
editText.isFocusable = true
editText.isEnabled = true
editText.addTextChangedListener { text: Editable? ->
task.title = text?.toString()
}
editText.setOnEditorActionListener { _, actionId: Int, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
if (editText.text.isNotEmpty()) {
@ -214,6 +161,7 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
private fun updateCompleteBox(task: Task, completeBox: CheckableImageView, editText: EditText) {
val isComplete = completeBox.isChecked
task.completionDate = if (isComplete) now() else 0
completeBox.setImageDrawable(
checkBoxProvider.getCheckBox(isComplete, false, task.priority))
editText.paintFlags = if (isComplete) {
@ -229,10 +177,10 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
private fun updateUI() {
if (isGoogleTaskChild) {
recyclerView!!.visibility = View.GONE
recyclerView.visibility = View.GONE
newSubtaskContainer.visibility = View.GONE
} else {
recyclerView!!.visibility = View.VISIBLE
recyclerView.visibility = View.VISIBLE
newSubtaskContainer.visibility = View.VISIBLE
recyclerAdapter.setMultiLevelSubtasksEnabled(remoteList !is GtasksFilter)
refresh()
@ -241,13 +189,11 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
fun onRemoteListChanged(filter: Filter?) {
remoteList = filter
if (recyclerView != null) {
updateUI()
}
updateUI()
}
private fun refresh() {
viewModel.invalidate()
listViewModel.invalidate()
}
override fun openSubtask(task: Task) {
@ -273,7 +219,6 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
companion object {
const val TAG = R.string.TEA_ctrl_subtask_pref
private const val EXTRA_NEW_SUBTASKS = "extra_new_subtasks"
private fun getQueryTemplate(task: Task): QueryTemplate {
return QueryTemplate()
.join(

@ -1,6 +1,5 @@
package org.tasks.ui
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -8,21 +7,17 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.ViewModelProviders
import butterknife.ButterKnife
import com.todoroo.astrid.data.Task
import kotlinx.coroutines.launch
import org.tasks.R
abstract class TaskEditControlFragment : Fragment() {
protected lateinit var task: Task
var isNew = false
private set
lateinit var viewModel: TaskEditViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.control_set_template, null)
viewModel = ViewModelProviders.of(requireParentFragment()).get(TaskEditViewModel::class.java)
val content = view.findViewById<LinearLayout>(R.id.content)
inflater.inflate(layout, content)
val icon = view.findViewById<ImageView>(R.id.icon)
@ -32,21 +27,12 @@ abstract class TaskEditControlFragment : Fragment() {
}
ButterKnife.bind(this, view)
lifecycleScope.launch {
createView(savedInstanceState)
}
createView(savedInstanceState)
return view
}
protected abstract fun createView(savedInstanceState: Bundle?)
override fun onAttach(activity: Activity) {
super.onAttach(activity)
val args = requireArguments()
task = args.getParcelable(EXTRA_TASK)!!
isNew = args.getBoolean(EXTRA_IS_NEW)
}
protected open fun createView(savedInstanceState: Bundle?) {}
protected open fun onRowClick() {}
protected open val isClickable: Boolean
@ -55,18 +41,4 @@ abstract class TaskEditControlFragment : Fragment() {
protected abstract val layout: Int
protected abstract val icon: Int
abstract fun controlId(): Int
open fun requiresId(): Boolean {
return false
}
abstract suspend fun apply(task: Task)
open suspend fun hasChanges(original: Task): Boolean {
return false
}
companion object {
const val EXTRA_TASK = "extra_task"
const val EXTRA_IS_NEW = "extra_is_new"
}
}

@ -0,0 +1,464 @@
package org.tasks.ui
import android.content.Context
import androidx.annotation.MainThread
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.ical.values.RRule
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.SyncFlags
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.createDueDate
import com.todoroo.astrid.data.Task.Companion.hasDueTime
import com.todoroo.astrid.data.Task.Companion.isRepeatAfterCompletion
import com.todoroo.astrid.data.Task.Companion.withoutFrom
import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.timers.TimerPlugin
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.tasks.Event
import org.tasks.R
import org.tasks.Strings
import org.tasks.calendars.CalendarEventProvider
import org.tasks.data.*
import org.tasks.location.GeofenceApi
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.currentTimeMillis
import timber.log.Timber
import java.text.ParseException
class TaskEditViewModel @ViewModelInject constructor(
@ApplicationContext private val context: Context,
private val taskDao: TaskDao,
private val taskDeleter: TaskDeleter,
private val timerPlugin: TimerPlugin,
private val permissionChecker: PermissionChecker,
private val calendarEventProvider: CalendarEventProvider,
private val gCalHelper: GCalHelper,
private val taskMover: TaskMover,
private val locationDao: LocationDao,
private val geofenceApi: GeofenceApi,
private val tagDao: TagDao,
private val tagDataDao: TagDataDao,
private val preferences: Preferences,
private val defaultFilterProvider: DefaultFilterProvider,
private val googleTaskDao: GoogleTaskDao,
private val caldavDao: CaldavDao,
private val taskCompleter: TaskCompleter,
private val alarmDao: AlarmDao,
private val alarmService: AlarmService) : ViewModel() {
val cleared = MutableLiveData<Event<Boolean>>()
fun setup(task: Task) {
this.task = task
isNew = task.isNew
runBlocking {
val list = async { defaultFilterProvider.getList(task) }
val location = async { locationDao.getLocation(task, preferences) }
val tags = async { tagDataDao.getTags(task) }
val alarms = async { alarmDao.getAlarms(task) }
originalList = list.await()
originalLocation = location.await()
originalTags = tags.await().toImmutableList()
originalAlarms = alarms.await().map { it.time }.toImmutableSet()
if (isNew && permissionChecker.canAccessCalendars()) {
originalCalendar = preferences.defaultCalendar
}
}
}
var task: Task? = null
private set
var title: String? = null
get() = field ?: task?.title
var completed: Boolean? = null
get() = field ?: task?.isCompleted ?: false
var dueDate: Long? = null
get() = field ?: task?.dueDate ?: 0
set(value) {
field = when {
value == null -> null
value == 0L -> 0
hasDueTime(value) -> createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, value)
else -> createDueDate(Task.URGENCY_SPECIFIC_DAY, value)
}
}
var priority: Int? = null
get() = field ?: task?.priority ?: 0
var description: String? = null
get() = field ?: task?.notes.stripCarriageReturns()
var hideUntil: Long? = null
get() = field ?: task?.hideUntil ?: 0
var recurrence: String? = null
get() = field ?: task?.recurrence
var repeatUntil: Long? = null
get() = field ?: task?.repeatUntil ?: 0
var repeatAfterCompletion: Boolean? = null
get() = field ?: task?.repeatAfterCompletion() ?: false
set(value) {
field = value
if (value == true) {
if (!recurrence.isRepeatAfterCompletion()) {
recurrence += ";FROM=COMPLETION"
}
} else if (recurrence.isRepeatAfterCompletion()) {
recurrence = recurrence.withoutFrom()
}
}
var rrule: RRule?
get() = if (recurrence.isNullOrBlank()) {
null
} else {
val rrule = RRule(recurrence.withoutFrom())
rrule.until = DateTime(repeatUntil!!).toDateValue()
rrule
}
set(value) {
if (value == null) {
recurrence = ""
repeatUntil = 0
return
}
val copy: RRule = try {
RRule(value.toIcal())
} catch (e: ParseException) {
recurrence = ""
repeatUntil = 0
return
}
repeatUntil = DateTime.from(copy.until).millis
copy.until = null
var result = copy.toIcal()
if (repeatAfterCompletion!! && !result.isNullOrBlank()) {
result += ";FROM=COMPLETION"
}
recurrence = result
}
var originalCalendar: String? = null
private set(value) {
field = value
selectedCalendar = value
}
var selectedCalendar: String? = null
var eventUri: String?
get() = task?.calendarURI
set(value) {
task?.calendarURI = value
}
var isNew: Boolean = false
private set
var timerStarted: Long
get() = task?.timerStart ?: 0
set(value) {
task?.timerStart = value
}
var estimatedSeconds: Int? = null
get() = field ?: task?.estimatedSeconds ?: 0
var elapsedSeconds: Int? = null
get() = field ?: task?.elapsedSeconds ?: 0
var originalList: Filter? = null
private set(value) {
field = value
selectedList = value
}
var selectedList: Filter? = null
var originalLocation: Location? = null
private set(value) {
field = value
selectedLocation = value
}
var selectedLocation: Location? = null
var originalTags: ImmutableList<TagData>? = null
private set(value) {
field = value
selectedTags = value?.let { ArrayList(it) }
}
var selectedTags: ArrayList<TagData>? = null
var newSubtasks = ArrayList<Task>()
var reminderPeriod: Long? = null
get() = field ?: task?.reminderPeriod ?: 0
var originalAlarms: ImmutableSet<Long>? = null
private set(value) {
field = value
selectedAlarms = value?.let { HashSet(it) }
}
var selectedAlarms: HashSet<Long>? = null
var whenDue: Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_AT_DEADLINE) ?: 0 > 0)
var whenOverdue: Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_AFTER_DEADLINE) ?: 0 > 0)
var ringNonstop: Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_MODE_NONSTOP) ?: 0 > 0)
set(value) {
field = value
if (value == true) {
ringFiveTimes = false
}
}
var ringFiveTimes:Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_MODE_FIVE) ?: 0 > 0)
set(value) {
field = value
if (value == true) {
ringNonstop = false
}
}
fun hasChanges(): Boolean = task?.let {
(it.title != title || (isNew && title?.isNotBlank() == true)) ||
it.isCompleted != completed ||
it.dueDate != dueDate ||
it.priority != priority ||
it.notes != description ||
it.hideUntil != hideUntil ||
if (it.recurrence.isNullOrBlank()) {
!recurrence.isNullOrBlank()
} else {
it.recurrence != recurrence
} ||
it.repeatAfterCompletion() != repeatAfterCompletion ||
it.repeatUntil != repeatUntil ||
originalCalendar != selectedCalendar ||
it.calendarURI != eventUri ||
it.elapsedSeconds != elapsedSeconds ||
it.estimatedSeconds != estimatedSeconds ||
originalList != selectedList ||
originalLocation != selectedLocation ||
originalTags?.toHashSet() != selectedTags?.toHashSet() ||
newSubtasks.isNotEmpty() ||
it.reminderPeriod != reminderPeriod ||
it.reminderFlags != getReminderFlags() ||
originalAlarms != selectedAlarms
} ?: false
fun cleared() = cleared.value?.value == true
@MainThread
suspend fun save(): Boolean = task?.let {
if (cleared()) {
return false
}
if (!hasChanges()) {
discard()
return false
}
clear()
it.title = if (title.isNullOrBlank()) context.getString(R.string.no_title) else title
it.dueDate = dueDate!!
it.priority = priority!!
it.notes = description
it.hideUntil = hideUntil!!
it.recurrence = recurrence
it.repeatUntil = repeatUntil!!
it.elapsedSeconds = elapsedSeconds!!
it.estimatedSeconds = estimatedSeconds!!
it.reminderFlags = getReminderFlags()
it.reminderPeriod = reminderPeriod!!
applyCalendarChanges()
val isNew = it.isNew
if (isNew) {
taskDao.createNew(it)
}
if (isNew || originalList != selectedList) {
it.parent = 0
taskMover.move(listOf(it.id), selectedList!!)
}
if ((isNew && selectedLocation != null) || originalLocation != selectedLocation) {
originalLocation?.let { location ->
if (location.geofence.id > 0) {
locationDao.delete(location.geofence)
geofenceApi.update(location.place)
}
}
selectedLocation?.let { location ->
val place = location.place
val geofence = location.geofence
geofence.task = it.id
geofence.place = place.uid
geofence.id = locationDao.insert(geofence)
geofenceApi.update(place)
}
it.putTransitory(SyncFlags.FORCE_CALDAV_SYNC, true)
it.modificationDate = currentTimeMillis()
}
if (originalTags?.toHashSet() != selectedTags?.toHashSet()) {
tagDao.applyTags(it, tagDataDao, selectedTags!!)
it.modificationDate = currentTimeMillis()
}
for (subtask in newSubtasks) {
if (Strings.isNullOrEmpty(subtask.title)) {
continue
}
if (!subtask.isCompleted) {
subtask.completionDate = it.completionDate
}
taskDao.createNew(subtask)
when (selectedList) {
is GtasksFilter -> {
val googleTask = GoogleTask(subtask.id, (selectedList as GtasksFilter).remoteId)
googleTask.parent = it.id
googleTask.isMoved = true
googleTaskDao.insertAndShift(googleTask, false)
}
is CaldavFilter -> {
val caldavTask = CaldavTask(subtask.id, (selectedList as CaldavFilter).uuid)
subtask.parent = it.id
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(it.id)
taskDao.save(subtask)
caldavDao.insert(subtask, caldavTask, false)
}
else -> {
subtask.parent = it.id
taskDao.save(subtask)
}
}
}
if (selectedAlarms != originalAlarms) {
alarmService.synchronizeAlarms(it.id, selectedAlarms!!)
it.modificationDate = now()
}
taskDao.save(it, null)
if (it.isCompleted != completed!!) {
taskCompleter.setComplete(it, completed!!)
}
true
} ?: false
private fun applyCalendarChanges() {
if (!permissionChecker.canAccessCalendars()) {
return
}
if (eventUri == null || task?.hasDueDate() != true) {
if (!task?.calendarURI.isNullOrBlank()) {
calendarEventProvider.deleteEvent(task)
}
}
eventUri?.let {
if (!it.isBlank()) {
gCalHelper.updateEvent(it, task!!)
}
}
selectedCalendar?.let {
try {
task?.calendarURI = gCalHelper.createTaskEvent(task, it)?.toString()
} catch (e: Exception) {
Timber.e(e)
}
}
}
private fun getReminderFlags(): Int {
var value = 0
if (whenDue == true) {
value = value or Task.NOTIFY_AT_DEADLINE
}
if (whenOverdue == true) {
value = value or Task.NOTIFY_AFTER_DEADLINE
}
value = value and (Task.NOTIFY_MODE_FIVE or Task.NOTIFY_MODE_NONSTOP).inv()
if (ringNonstop == true) {
value = value or Task.NOTIFY_MODE_NONSTOP
} else if (ringFiveTimes == true) {
value = value or Task.NOTIFY_MODE_FIVE
}
return value
}
fun delete() {
task?.let(taskDeleter::markDeleted)
discard()
}
fun discard() {
task?.let {
if (it.isNew) {
timerPlugin.stopTimer(it)
}
}
clear()
}
@MainThread
fun clear() {
if (!cleared()) {
cleared.value = Event(true)
}
}
override fun onCleared() {
cleared.value.let {
if (it == null || !it.value) {
runBlocking(NonCancellable) {
save()
}
}
}
}
companion object {
fun String?.stripCarriageReturns(): String? {
return this?.replace("\\r\\n?".toRegex(), "\n")
}
}
}

@ -0,0 +1,28 @@
package org.tasks.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.ui.TaskEditViewModel.Companion.stripCarriageReturns
class TaskEditViewModelTest {
@Test
fun replaceCRLF() {
assertEquals("aaa\nbbb", "aaa\r\nbbb".stripCarriageReturns())
}
@Test
fun replaceCR() {
assertEquals("aaa\nbbb", "aaa\rbbb".stripCarriageReturns())
}
@Test
fun dontReplaceLF() {
assertEquals("aaa\nbbb", "aaa\nbbb".stripCarriageReturns())
}
@Test
fun stripCarriageReturnOnNull() {
assertNull((null as String?).stripCarriageReturns())
}
}
Loading…
Cancel
Save