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 SNOOZE_TIME: Property<Task, DateTime?> = newProperty()
val RRULE: Property<Task, RRule?> = newProperty() val RRULE: Property<Task, RRule?> = newProperty()
val AFTER_COMPLETE: Property<Task, Boolean> = newProperty() val AFTER_COMPLETE: Property<Task, Boolean> = newProperty()
private val TITLE: Property<Task, String?> = newProperty() val TITLE: Property<Task, String?> = newProperty()
private val PRIORITY: Property<Task, Int> = newProperty() val PRIORITY: Property<Task, Int> = newProperty()
val PARENT: Property<Task, Long> = newProperty() val PARENT: Property<Task, Long> = newProperty()
val UUID: Property<Task, String> = newProperty() val UUID: Property<Task, String> = newProperty()

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

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

@ -502,11 +502,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
private fun onTaskDelete(task: Task) { private fun onTaskDelete(task: Task) {
val activity = activity as MainActivity? (activity as MainActivity?)?.taskEditFragment?.let {
if (activity != null) { if (task.id == it.editViewModel.task?.id) {
val tef = activity.taskEditFragment it.editViewModel.discard()
if (tef != null && task.id == tef.model.id) {
tef.discard()
} }
} }
timerPlugin.stopTimer(task) 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)) { if (!task.insignificantChange(original)) {
task.modificationDate = DateUtilities.now() task.modificationDate = DateUtilities.now()
} }
if (task.dueDate != original?.dueDate) {
task.reminderSnooze = 0
}
if (update(task) == 1) { if (update(task) == 1) {
workManager.afterSave(task, original) 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)) { if (Task.isUuidEmpty(task.remoteId)) {
task.remoteId = UUIDHelper.newUUID() task.remoteId = UUIDHelper.newUUID()
} }
if (BuildConfig.DEBUG) {
require(task.remoteId?.isNotBlank() == true && task.remoteId != "0")
}
val insert = insert(task) val insert = insert(task)
task.id = insert task.id = insert
} }

@ -11,7 +11,6 @@ import com.todoroo.andlib.data.Table
import com.todoroo.andlib.sql.Field import com.todoroo.andlib.sql.Field
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.dao.TaskDaoBlocking
import org.tasks.Strings import org.tasks.Strings
import org.tasks.backup.XmlReader import org.tasks.backup.XmlReader
import org.tasks.data.Tag import org.tasks.data.Tag
@ -233,11 +232,11 @@ class Task : Parcelable {
return dueDate < compareTo && !isCompleted 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 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) { fun setDueDateAdjustingHideUntil(newDueDate: Long) {
if (dueDate > 0) { if (dueDate > 0) {
@ -612,5 +611,9 @@ class Task : Parcelable {
@JvmStatic fun isUuidEmpty(uuid: String?): Boolean { @JvmStatic fun isUuidEmpty(uuid: String?): Boolean {
return NO_UUID == uuid || Strings.isNullOrEmpty(uuid) 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 androidx.lifecycle.lifecycleScope
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick import butterknife.OnClick
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -45,10 +44,8 @@ class FilesControlSet : TaskEditControlFragment() {
@BindView(R.id.add_attachment) @BindView(R.id.add_attachment)
lateinit var addAttachment: TextView lateinit var addAttachment: TextView
private var taskUuid: String? = null
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
taskUuid = task.uuid val task = viewModel.task!!
if (savedInstanceState == null) { if (savedInstanceState == null) {
if (task.hasTransitory(TaskAttachment.KEY)) { if (task.hasTransitory(TaskAttachment.KEY)) {
for (uri in (task.getTransitory<ArrayList<Uri>>(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) AddAttachmentDialog.newAddAttachmentDialog(this).show(parentFragmentManager, FRAG_TAG_ADD_ATTACHMENT_DIALOG)
} }
override val layout: Int override val layout = R.layout.control_set_files
get() = R.layout.control_set_files
override val icon: Int override val icon = R.drawable.ic_outline_attachment_24px
get() = R.drawable.ic_outline_attachment_24px
override fun controlId() = TAG override fun controlId() = TAG
override suspend fun apply(task: Task) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AddAttachmentDialog.REQUEST_CAMERA || requestCode == AddAttachmentDialog.REQUEST_AUDIO) { if (requestCode == AddAttachmentDialog.REQUEST_CAMERA || requestCode == AddAttachmentDialog.REQUEST_AUDIO) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
@ -143,7 +136,10 @@ class FilesControlSet : TaskEditControlFragment() {
} }
private fun newAttachment(output: Uri) { 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 { lifecycleScope.launch {
taskAttachmentDao.createNew(attachment) taskAttachmentDao.createNew(attachment)
addAttachment(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); return createTaskEvent(task, values, true);
} }
@ -127,6 +129,19 @@ public class GCalHelper {
return null; 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) { public void rescheduleRepeatingTask(Task task) {
String taskUri = getTaskEventUri(task); String taskUri = getTaskEventUri(task);
if (isNullOrEmpty(taskUri)) { if (isNullOrEmpty(taskUri)) {

@ -18,20 +18,17 @@ import butterknife.OnItemSelected
import com.google.ical.values.Frequency import com.google.ical.values.Frequency
import com.google.ical.values.RRule import com.google.ical.values.RRule
import com.google.ical.values.WeekdayNum import com.google.ical.values.WeekdayNum
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.repeats.BasicRecurrenceDialog import org.tasks.repeats.BasicRecurrenceDialog
import org.tasks.repeats.RepeatRuleToString import org.tasks.repeats.RepeatRuleToString
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.time.DateTime 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.HiddenTopArrayAdapter
import org.tasks.ui.TaskEditControlFragment import org.tasks.ui.TaskEditControlFragment
import java.text.ParseException
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -59,20 +56,17 @@ class RepeatControlSet : TaskEditControlFragment() {
@BindView(R.id.repeatTypeContainer) @BindView(R.id.repeatTypeContainer)
lateinit var repeatTypeContainer: LinearLayout lateinit var repeatTypeContainer: LinearLayout
private var rrule: RRule? = null
private lateinit var typeAdapter: HiddenTopArrayAdapter<String> private lateinit var typeAdapter: HiddenTopArrayAdapter<String>
private var dueDate: Long = 0
private var repeatAfterCompletion = false
fun onSelected(rrule: RRule?) { fun onSelected(rrule: RRule?) {
this.rrule = rrule viewModel.rrule = rrule
refreshDisplayView() refreshDisplayView()
} }
fun onDueDateChanged(dueDate: Long) { fun onDueDateChanged() {
this.dueDate = if (dueDate > 0) dueDate else DateTimeUtils.currentTimeMillis() viewModel.rrule?.let {
if (rrule != null && rrule!!.freq == Frequency.MONTHLY && rrule!!.byDay.isNotEmpty()) { if (it.freq == Frequency.MONTHLY && it.byDay.isNotEmpty()) {
val weekdayNum = rrule!!.byDay[0] val weekdayNum = it.byDay[0]
val dateTime = DateTime(this.dueDate) val dateTime = DateTime(this.dueDate)
val num: Int val num: Int
val dayOfWeekInMonth = dateTime.dayOfWeekInMonth val dayOfWeekInMonth = dateTime.dayOfWeekInMonth
@ -81,43 +75,14 @@ class RepeatControlSet : TaskEditControlFragment() {
} else { } else {
dayOfWeekInMonth dayOfWeekInMonth
} }
rrule!!.byDay = listOf((WeekdayNum(num, dateTime.weekday))) it.byDay = listOf((WeekdayNum(num, dateTime.weekday)))
viewModel.rrule = it
refreshDisplayView() refreshDisplayView()
} }
} }
}
override fun createView(savedInstanceState: Bundle?) { 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.add("")
repeatTypes.addAll(listOf(*resources.getStringArray(R.array.repeat_type))) repeatTypes.addAll(listOf(*resources.getStringArray(R.array.repeat_type)))
typeAdapter = object : HiddenTopArrayAdapter<String>(activity, 0, repeatTypes) { typeAdapter = object : HiddenTopArrayAdapter<String>(activity, 0, repeatTypes) {
@ -136,86 +101,49 @@ class RepeatControlSet : TaskEditControlFragment() {
drawable.setTint(activity.getColor(R.color.text_primary)) drawable.setTint(activity.getColor(R.color.text_primary))
typeSpinner.setBackgroundDrawable(drawable) typeSpinner.setBackgroundDrawable(drawable)
typeSpinner.adapter = typeAdapter 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() refreshDisplayView()
} }
@OnItemSelected(R.id.repeatType) @OnItemSelected(R.id.repeatType)
fun onRepeatTypeChanged(position: Int) { fun onRepeatTypeChanged(position: Int) {
repeatAfterCompletion = position == TYPE_COMPLETION_DATE viewModel.repeatAfterCompletion = position == TYPE_COMPLETION_DATE
repeatTypes[0] = if (repeatAfterCompletion) repeatTypes[2] else repeatTypes[1] repeatTypes[0] = if (viewModel.repeatAfterCompletion!!) repeatTypes[2] else repeatTypes[1]
typeAdapter.notifyDataSetChanged() typeAdapter.notifyDataSetChanged()
} }
override fun onSaveInstanceState(outState: Bundle) { private val dueDate: Long
super.onSaveInstanceState(outState) get() = viewModel.dueDate!!.let { if (it > 0) it else currentTimeMillis() }
outState.putString(EXTRA_RECURRENCE, if (rrule == null) "" else rrule!!.toIcal())
outState.putBoolean(EXTRA_REPEAT_AFTER_COMPLETION, repeatAfterCompletion)
outState.putLong(EXTRA_DUE_DATE, dueDate)
}
override fun onRowClick() { override fun onRowClick() {
BasicRecurrenceDialog.newBasicRecurrenceDialog(this, rrule, dueDate) BasicRecurrenceDialog.newBasicRecurrenceDialog(this, viewModel.rrule, dueDate)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE) .show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
} }
override val isClickable: Boolean override val isClickable = true
get() = true
override val layout: Int override val layout = R.layout.control_set_repeat_display
get() = R.layout.control_set_repeat_display
override val icon: Int override val icon = R.drawable.ic_outline_repeat_24px
get() = R.drawable.ic_outline_repeat_24px
override fun controlId() = TAG 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() { private fun refreshDisplayView() {
if (rrule == null) { viewModel.rrule.let {
if (it == null) {
displayView.text = null displayView.text = null
repeatTypeContainer.visibility = View.GONE repeatTypeContainer.visibility = View.GONE
} else { } else {
displayView.text = repeatRuleToString.toString(rrule) displayView.text = repeatRuleToString.toString(it)
repeatTypeContainer.visibility = View.VISIBLE repeatTypeContainer.visibility = View.VISIBLE
} }
} }
}
companion object { companion object {
const val TAG = R.string.TEA_ctrl_repeat_pref const val TAG = R.string.TEA_ctrl_repeat_pref
private const val TYPE_DUE_DATE = 1 private const val TYPE_DUE_DATE = 1
private const val TYPE_COMPLETION_DATE = 2 private const val TYPE_COMPLETION_DATE = 2
private const val FRAG_TAG_BASIC_RECURRENCE = "frag_tag_basic_recurrence" 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 com.todoroo.astrid.helper.UUIDHelper.newUUID;
import static org.tasks.Strings.isNullOrEmpty; import static org.tasks.Strings.isNullOrEmpty;
import android.content.ContentValues;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
@ -87,7 +86,7 @@ public class TaskCreator {
if (!isNullOrEmpty(task.getTitle()) if (!isNullOrEmpty(task.getTitle())
&& gcalCreateEventEnabled && gcalCreateEventEnabled
&& isNullOrEmpty(task.getCalendarURI())) { && isNullOrEmpty(task.getCalendarURI())) {
Uri calendarUri = gcalHelper.createTaskEvent(task, new ContentValues()); Uri calendarUri = gcalHelper.createTaskEvent(task, preferences.getDefaultCalendar());
task.setCalendarURI(calendarUri.toString()); task.setCalendarURI(calendarUri.toString());
} }

@ -12,18 +12,12 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import butterknife.BindView import butterknife.BindView
import com.google.android.material.chip.ChipGroup 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 dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.data.TagDao
import org.tasks.data.TagData import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.tags.TagPickerActivity import org.tasks.tags.TagPickerActivity
import org.tasks.ui.ChipProvider import org.tasks.ui.ChipProvider
import org.tasks.ui.TaskEditControlFragment import org.tasks.ui.TaskEditControlFragment
import java.util.*
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -33,8 +27,6 @@ import javax.inject.Inject
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class TagsControlSet : TaskEditControlFragment() { class TagsControlSet : TaskEditControlFragment() {
@Inject lateinit var tagDao: TagDao
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
@BindView(R.id.no_tags) @BindView(R.id.no_tags)
@ -43,55 +35,26 @@ class TagsControlSet : TaskEditControlFragment() {
@BindView(R.id.chip_group) @BindView(R.id.chip_group)
lateinit var chipGroup: ChipGroup lateinit var chipGroup: ChipGroup
private lateinit var originalTags: ArrayList<TagData>
private lateinit var selectedTags: ArrayList<TagData>
override fun createView(savedInstanceState: Bundle?) { 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() 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()
}
}
override fun onRowClick() { override fun onRowClick() {
val intent = Intent(context, TagPickerActivity::class.java) 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) startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY)
} }
override val isClickable: Boolean override val layout = R.layout.control_set_tags
get() = true
override val icon: Int override val isClickable = true
get() = R.drawable.ic_outline_label_24px
override fun controlId() = TAG override val icon = R.drawable.ic_outline_label_24px
override suspend fun hasChanges(original: Task): Boolean { override fun controlId() = TAG
return HashSet(originalTags) != HashSet(selectedTags)
}
private fun refreshDisplayView() { private fun refreshDisplayView() {
viewModel.selectedTags?.let { selectedTags ->
if (selectedTags.isEmpty()) { if (selectedTags.isEmpty()) {
chipGroup.visibility = View.GONE chipGroup.visibility = View.GONE
tagsDisplay.visibility = View.VISIBLE tagsDisplay.visibility = View.VISIBLE
@ -111,11 +74,13 @@ class TagsControlSet : TaskEditControlFragment() {
} }
} }
} }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) { if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) {
if (resultCode == Activity.RESULT_OK && data != null) { if (resultCode == Activity.RESULT_OK && data != null) {
selectedTags = data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!! viewModel.selectedTags =
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)
refreshDisplayView() refreshDisplayView()
} }
} else { } else {
@ -123,12 +88,8 @@ class TagsControlSet : TaskEditControlFragment() {
} }
} }
override fun requiresId() = true
companion object { companion object {
const val TAG = R.string.TEA_ctrl_lists_pref 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 private const val REQUEST_TAG_PICKER_ACTIVITY = 10582
} }
} }

@ -51,28 +51,16 @@ class TimerControlSet : TaskEditControlFragment() {
private lateinit var estimated: TimeDurationControlSet private lateinit var estimated: TimeDurationControlSet
private lateinit var elapsed: TimeDurationControlSet private lateinit var elapsed: TimeDurationControlSet
private var timerStarted: Long = 0
private var dialog: AlertDialog? = null private var dialog: AlertDialog? = null
private lateinit var dialogView: View private lateinit var dialogView: View
private lateinit var callback: TimerControlSetCallback private lateinit var callback: TimerControlSetCallback
override fun createView(savedInstanceState: Bundle?) { 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) dialogView = activity.layoutInflater.inflate(R.layout.control_set_timers_dialog, null)
estimated = TimeDurationControlSet(activity, dialogView, R.id.estimatedDuration, theme) estimated = TimeDurationControlSet(activity, dialogView, R.id.estimatedDuration, theme)
elapsed = TimeDurationControlSet(activity, dialogView, R.id.elapsedDuration, theme) elapsed = TimeDurationControlSet(activity, dialogView, R.id.elapsedDuration, theme)
estimated.setTimeDuration(estimatedSeconds) estimated.setTimeDuration(viewModel.estimatedSeconds!!)
elapsed.setTimeDuration(elapsedSeconds) elapsed.setTimeDuration(viewModel.elapsedSeconds!!)
refresh() refresh()
} }
@ -81,13 +69,6 @@ class TimerControlSet : TaskEditControlFragment() {
callback = activity as TimerControlSetCallback 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() { override fun onRowClick() {
if (dialog == null) { if (dialog == null) {
dialog = buildDialog() dialog = buildDialog()
@ -95,9 +76,6 @@ class TimerControlSet : TaskEditControlFragment() {
dialog!!.show() dialog!!.show()
} }
override val isClickable: Boolean
get() = true
private fun buildDialog(): AlertDialog { private fun buildDialog(): AlertDialog {
return dialogBuilder return dialogBuilder
.newDialog() .newDialog()
@ -112,34 +90,24 @@ class TimerControlSet : TaskEditControlFragment() {
if (timerActive()) { if (timerActive()) {
val task = callback.stopTimer() val task = callback.stopTimer()
elapsed.setTimeDuration(task.elapsedSeconds) elapsed.setTimeDuration(task.elapsedSeconds)
timerStarted = 0 viewModel.timerStarted = 0
chronometer.stop() chronometer.stop()
refreshDisplayView() refreshDisplayView()
} else { } else {
val task = callback.startTimer() val task = callback.startTimer()
timerStarted = task.timerStart viewModel.timerStarted = task.timerStart
chronometer.start() chronometer.start()
} }
updateChronometer() updateChronometer()
} }
override val layout: Int override val layout = R.layout.control_set_timers
get() = R.layout.control_set_timers
override val icon: Int override val icon = R.drawable.ic_outline_timer_24px
get() = R.drawable.ic_outline_timer_24px
override fun controlId() = TAG override fun controlId() = TAG
override suspend fun hasChanges(original: Task): Boolean { override val isClickable = true
return (elapsed.timeDurationInSeconds != original.elapsedSeconds
|| estimated.timeDurationInSeconds != original.estimatedSeconds)
}
override suspend fun apply(task: Task) {
task.elapsedSeconds = elapsed.timeDurationInSeconds
task.estimatedSeconds = estimated.timeDurationInSeconds
}
private fun refresh() { private fun refresh() {
refreshDisplayView() refreshDisplayView()
@ -148,14 +116,18 @@ class TimerControlSet : TaskEditControlFragment() {
private fun refreshDisplayView() { private fun refreshDisplayView() {
var est: String? = null var est: String? = null
val estimatedSeconds = estimated.timeDurationInSeconds viewModel.estimatedSeconds = estimated.timeDurationInSeconds
if (estimatedSeconds > 0) { if (viewModel.estimatedSeconds!! > 0) {
est = getString(R.string.TEA_timer_est, DateUtils.formatElapsedTime(estimatedSeconds.toLong())) est = getString(
R.string.TEA_timer_est,
DateUtils.formatElapsedTime(viewModel.estimatedSeconds!!.toLong()))
} }
var elap: String? = null var elap: String? = null
val elapsedSeconds = elapsed.timeDurationInSeconds viewModel.elapsedSeconds = elapsed.timeDurationInSeconds
if (elapsedSeconds > 0) { if (viewModel.elapsedSeconds!! > 0) {
elap = getString(R.string.TEA_timer_elap, DateUtils.formatElapsedTime(elapsedSeconds.toLong())) elap = getString(
R.string.TEA_timer_elap,
DateUtils.formatElapsedTime(viewModel.elapsedSeconds!!.toLong()))
} }
val toDisplay: String? val toDisplay: String?
toDisplay = if (!isNullOrEmpty(est) && !isNullOrEmpty(elap)) { toDisplay = if (!isNullOrEmpty(est) && !isNullOrEmpty(elap)) {
@ -176,7 +148,7 @@ class TimerControlSet : TaskEditControlFragment() {
var elapsed = elapsed.timeDurationInSeconds * 1000L var elapsed = elapsed.timeDurationInSeconds * 1000L
if (timerActive()) { if (timerActive()) {
chronometer.visibility = View.VISIBLE chronometer.visibility = View.VISIBLE
elapsed += DateUtilities.now() - timerStarted elapsed += DateUtilities.now() - viewModel.timerStarted
chronometer.base = SystemClock.elapsedRealtime() - elapsed chronometer.base = SystemClock.elapsedRealtime() - elapsed
if (elapsed > DateUtilities.ONE_DAY) { if (elapsed > DateUtilities.ONE_DAY) {
chronometer.onChronometerTickListener = OnChronometerTickListener { cArg: Chronometer -> chronometer.onChronometerTickListener = OnChronometerTickListener { cArg: Chronometer ->
@ -191,9 +163,7 @@ class TimerControlSet : TaskEditControlFragment() {
} }
} }
private fun timerActive(): Boolean { private fun timerActive() = viewModel.timerStarted > 0
return timerStarted > 0
}
interface TimerControlSetCallback { interface TimerControlSetCallback {
fun stopTimer(): Task fun stopTimer(): Task
@ -202,8 +172,5 @@ class TimerControlSet : TaskEditControlFragment() {
companion object { companion object {
const val TAG = R.string.TEA_ctrl_timer_pref 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() spinner.performClick()
} }
override val isClickable: Boolean
get() = true
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
adapter = object : HiddenTopArrayAdapter<HideUntilValue>( adapter = object : HiddenTopArrayAdapter<HideUntilValue>(
activity, android.R.layout.simple_spinner_item, spinnerItems) { activity, android.R.layout.simple_spinner_item, spinnerItems) {
@ -94,8 +91,8 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
} }
} }
if (savedInstanceState == null) { if (savedInstanceState == null) {
val dueDate = task.dueDate val dueDate = viewModel.dueDate!!
var hideUntil = task.hideUntil var hideUntil = viewModel.hideUntil!!
val dueDay = DateTimeUtils.newDateTime(dueDate) val dueDay = DateTimeUtils.newDateTime(dueDate)
.withHourOfDay(0) .withHourOfDay(0)
.withMinuteOfHour(0) .withMinuteOfHour(0)
@ -107,7 +104,7 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
if (hideUntil <= 0) { if (hideUntil <= 0) {
selection = 0 selection = 0
hideUntil = 0 hideUntil = 0
if (task.isNew) { if (viewModel.isNew) {
when (preferences.getIntegerFromString(R.string.p_default_hideUntil_key, Task.HIDE_UNTIL_NONE)) { when (preferences.getIntegerFromString(R.string.p_default_hideUntil_key, Task.HIDE_UNTIL_NONE)) {
Task.HIDE_UNTIL_DUE -> selection = 1 Task.HIDE_UNTIL_DUE -> selection = 1
Task.HIDE_UNTIL_DAY_BEFORE -> selection = 3 Task.HIDE_UNTIL_DAY_BEFORE -> selection = 3
@ -138,15 +135,13 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
refreshDisplayView() refreshDisplayView()
} }
override val layout: Int override val layout = R.layout.control_set_hide
get() = R.layout.control_set_hide
override val icon: Int override val icon = R.drawable.ic_outline_visibility_off_24px
get() = R.drawable.ic_outline_visibility_off_24px
override fun controlId(): Int { override fun controlId() = TAG
return TAG
} override val isClickable = true
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_HIDE_UNTIL) { 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) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putLong(EXTRA_CUSTOM, existingDate) outState.putLong(EXTRA_CUSTOM, existingDate)
@ -251,6 +234,8 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
private fun refreshDisplayView() { private fun refreshDisplayView() {
selectedValue = adapter.getItem(selection) 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 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 android.widget.Spinner
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import org.tasks.R import org.tasks.R
import org.tasks.ui.TaskEditViewModel
/** /**
* Control set dealing with random reminder settings * Control set dealing with random reminder settings
* *
* @author Tim Su <tim></tim>@todoroo.com> * @author Tim Su <tim></tim>@todoroo.com>
*/ */
internal class RandomReminderControlSet(context: Context, parentView: View, reminderPeriod: Long) { internal class RandomReminderControlSet(context: Context, parentView: View, reminderPeriod: Long, vm: TaskEditViewModel) {
private val hours: IntArray
private var selectedIndex = 0
val reminderPeriod: Long
get() {
val hourValue = hours[selectedIndex]
return hourValue * DateUtilities.ONE_HOUR
}
init { init {
val periodSpinner = parentView.findViewById<Spinner>(R.id.reminder_random_interval) val periodSpinner = parentView.findViewById<Spinner>(R.id.reminder_random_interval)
periodSpinner.visibility = View.VISIBLE 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 // create adapter
val adapter = ArrayAdapter( val adapter = ArrayAdapter(
context, context,
@ -39,19 +38,13 @@ internal class RandomReminderControlSet(context: Context, parentView: View, remi
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
periodSpinner.adapter = adapter periodSpinner.adapter = adapter
periodSpinner.onItemSelectedListener = object : OnItemSelectedListener { periodSpinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedIndex = position vm.reminderPeriod = hours[position] * DateUtilities.ONE_HOUR
} }
override fun onNothingSelected(parent: AdapterView<*>?) {} 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 var i = 0
while (i < hours.size - 1) { while (i < hours.size - 1) {
if (hours[i] * DateUtilities.ONE_HOUR >= reminderPeriod) { if (hours[i] * DateUtilities.ONE_HOUR >= reminderPeriod) {

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

@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import com.todoroo.astrid.data.Task
@Dao @Dao
interface AlarmDao { interface AlarmDao {
@ -28,4 +29,10 @@ interface AlarmDao {
@Insert @Insert
suspend fun insert(alarms: Iterable<Alarm>) 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() resetPictureButton()
} }
override val layout: Int override val layout = R.layout.fragment_comment_bar
get() = R.layout.fragment_comment_bar
override val icon: Int override val icon = 0
get() = 0
override fun controlId() = TAG override fun controlId() = TAG
override suspend fun apply(task: Task) {}
@OnTextChanged(R.id.commentField) @OnTextChanged(R.id.commentField)
fun onTextChanged(s: CharSequence) { fun onTextChanged(s: CharSequence) {
commentButton.visibility = if (pendingCommentPicture == null && isNullOrEmpty(s.toString())) View.GONE else View.VISIBLE commentButton.visibility = if (pendingCommentPicture == null && isNullOrEmpty(s.toString())) View.GONE else View.VISIBLE

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

@ -1,19 +1,14 @@
package org.tasks.ui package org.tasks.ui
import android.app.Activity import android.app.Activity
import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.provider.CalendarContract import android.provider.CalendarContract
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick import butterknife.OnClick
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.gcal.GCalHelper import com.todoroo.astrid.gcal.GCalHelper
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.PermissionUtil.verifyPermissions import org.tasks.PermissionUtil.verifyPermissions
@ -50,108 +45,33 @@ class CalendarControlSet : TaskEditControlFragment() {
@Inject lateinit var themeBase: ThemeBase @Inject lateinit var themeBase: ThemeBase
@Inject lateinit var calendarEventProvider: CalendarEventProvider @Inject lateinit var calendarEventProvider: CalendarEventProvider
private var calendarId: String? = null override fun onResume() {
private var eventUri: String? = null super.onResume()
override fun createView(savedInstanceState: Bundle?) {
val canAccessCalendars = permissionChecker.canAccessCalendars() val canAccessCalendars = permissionChecker.canAccessCalendars()
if (savedInstanceState != null) { viewModel.eventUri?.let {
eventUri = savedInstanceState.getString(EXTRA_URI) if (canAccessCalendars && !calendarEntryExists(it)) {
calendarId = savedInstanceState.getString(EXTRA_ID) viewModel.eventUri = null
} 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
}
} }
} else {
eventUri = task.calendarURI
} }
if (canAccessCalendars && !calendarEntryExists(eventUri)) { if (!canAccessCalendars) {
eventUri = null viewModel.selectedCalendar = null
} }
refreshDisplayView() refreshDisplayView()
} }
override val layout: Int override val layout = R.layout.control_set_gcal_display
get() = R.layout.control_set_gcal_display
override val icon: Int override val icon = R.drawable.ic_outline_event_24px
get() = R.drawable.ic_outline_event_24px
override fun controlId() = TAG override fun controlId() = TAG
override suspend fun hasChanges(original: Task): Boolean { override val isClickable = true
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)
}
@OnClick(R.id.clear) @OnClick(R.id.clear)
fun clearCalendar() { fun clearCalendar() {
if (isNullOrEmpty(eventUri)) { if (viewModel.eventUri.isNullOrBlank()) {
clear() clear()
} else { } else {
dialogBuilder dialogBuilder
@ -167,13 +87,13 @@ class CalendarControlSet : TaskEditControlFragment() {
} }
private fun clear() { private fun clear() {
calendarId = null viewModel.selectedCalendar = null
eventUri = null viewModel.eventUri = null
refreshDisplayView() refreshDisplayView()
} }
override fun onRowClick() { override fun onRowClick() {
if (isNullOrEmpty(eventUri)) { if (viewModel.eventUri.isNullOrBlank()) {
CalendarPicker.newCalendarPicker(this, REQUEST_CODE_PICK_CALENDAR, calendarName) CalendarPicker.newCalendarPicker(this, REQUEST_CODE_PICK_CALENDAR, calendarName)
.show(parentFragmentManager, FRAG_TAG_CALENDAR_PICKER) .show(parentFragmentManager, FRAG_TAG_CALENDAR_PICKER)
} else { } else {
@ -183,12 +103,9 @@ class CalendarControlSet : TaskEditControlFragment() {
} }
} }
override val isClickable: Boolean
get() = true
private fun openCalendarEvent() { private fun openCalendarEvent() {
val cr = activity.contentResolver val cr = activity.contentResolver
val uri = Uri.parse(eventUri) val uri = Uri.parse(viewModel.eventUri)
val intent = Intent(Intent.ACTION_VIEW, uri) val intent = Intent(Intent.ACTION_VIEW, uri)
try { try {
cr.query( cr.query(
@ -199,7 +116,7 @@ class CalendarControlSet : TaskEditControlFragment() {
if (cursor!!.count == 0) { if (cursor!!.count == 0) {
// event no longer exists // event no longer exists
Toast.makeText(activity, R.string.calendar_event_not_found, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.calendar_event_not_found, Toast.LENGTH_SHORT).show()
eventUri = null viewModel.eventUri = null
refreshDisplayView() refreshDisplayView()
} else { } else {
cursor.moveToFirst() cursor.moveToFirst()
@ -217,7 +134,7 @@ class CalendarControlSet : TaskEditControlFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PICK_CALENDAR) { if (requestCode == REQUEST_CODE_PICK_CALENDAR) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
calendarId = data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID) viewModel.selectedCalendar = data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID)
refreshDisplayView() refreshDisplayView()
} }
} else { } else {
@ -226,13 +143,7 @@ class CalendarControlSet : TaskEditControlFragment() {
} }
private val calendarName: String? private val calendarName: String?
get() { get() = viewModel.selectedCalendar?.let { calendarProvider.getCalendar(it)?.name }
if (calendarId == null) {
return null
}
val calendar = calendarProvider.getCalendar(calendarId)
return calendar?.name
}
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) { requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
@ -249,14 +160,16 @@ class CalendarControlSet : TaskEditControlFragment() {
} }
} }
private fun refreshDisplayView() { private fun refreshDisplayView() = when {
if (!isNullOrEmpty(eventUri)) { viewModel.eventUri?.isNotBlank() == true -> {
calendar.setText(R.string.gcal_TEA_showCalendar_label) calendar.setText(R.string.gcal_TEA_showCalendar_label)
cancelButton.visibility = View.VISIBLE cancelButton.visibility = View.VISIBLE
} else if (calendarId != null) { }
!viewModel.selectedCalendar.isNullOrBlank() -> {
calendar.text = calendarName calendar.text = calendarName
cancelButton.visibility = View.GONE cancelButton.visibility = View.GONE
} else { }
else -> {
calendar.text = null calendar.text = null
cancelButton.visibility = View.GONE cancelButton.visibility = View.GONE
} }
@ -287,7 +200,5 @@ class CalendarControlSet : TaskEditControlFragment() {
private const val REQUEST_CODE_PICK_CALENDAR = 70 private const val REQUEST_CODE_PICK_CALENDAR = 70
private const val REQUEST_CODE_OPEN_EVENT = 71 private const val REQUEST_CODE_OPEN_EVENT = 71
private const val REQUEST_CODE_CLEAR_EVENT = 72 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 android.widget.TextView
import butterknife.BindView import butterknife.BindView
import com.todoroo.andlib.utility.DateUtilities 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 com.todoroo.astrid.data.Task.Companion.hasDueTime
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
@ -16,7 +14,6 @@ import org.tasks.dialogs.DateTimePicker
import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.locale.Locale import org.tasks.locale.Locale
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import java.time.format.FormatStyle import java.time.format.FormatStyle
import javax.inject.Inject import javax.inject.Inject
@ -30,7 +27,6 @@ class DeadlineControlSet : TaskEditControlFragment() {
lateinit var dueDate: TextView lateinit var dueDate: TextView
private lateinit var callback: DueDateChangeListener private lateinit var callback: DueDateChangeListener
private var date: Long = 0
override fun onAttach(activity: Activity) { override fun onAttach(activity: Activity) {
super.onAttach(activity) super.onAttach(activity)
@ -38,7 +34,6 @@ class DeadlineControlSet : TaskEditControlFragment() {
} }
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
date = savedInstanceState?.getLong(EXTRA_DATE) ?: task.dueDate
refreshDisplayView() refreshDisplayView()
} }
@ -48,42 +43,25 @@ class DeadlineControlSet : TaskEditControlFragment() {
newDateTimePicker( newDateTimePicker(
this, this,
REQUEST_DATE, REQUEST_DATE,
dueDateTime, viewModel.dueDate!!,
preferences.getBoolean(R.string.p_auto_dismiss_datetime_edit_screen, false)) preferences.getBoolean(R.string.p_auto_dismiss_datetime_edit_screen, false))
.show(fragmentManager, FRAG_TAG_DATE_PICKER) .show(fragmentManager, FRAG_TAG_DATE_PICKER)
} }
} }
override val isClickable: Boolean override val isClickable = true
get() = true
override val layout: Int override val layout = R.layout.control_set_deadline
get() = R.layout.control_set_deadline
override val icon: Int override val icon = R.drawable.ic_outline_schedule_24px
get() = R.drawable.ic_outline_schedule_24px
override fun controlId() = TAG 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_DATE) { if (requestCode == REQUEST_DATE) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
val timestamp = data!!.getLongExtra(DateTimePicker.EXTRA_TIMESTAMP, 0L) viewModel.dueDate = data!!.getLongExtra(DateTimePicker.EXTRA_TIMESTAMP, 0L)
val dateTime = DateTime(timestamp) callback.dueDateChanged()
date = dateTime.millis
callback.dueDateChanged(dueDateTime)
} }
refreshDisplayView() refreshDisplayView()
} else { } 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() { private fun refreshDisplayView() {
val date = viewModel.dueDate!!
if (date == 0L) { if (date == 0L) {
dueDate.text = "" dueDate.text = ""
setTextColor(false) setTextColor(false)
} else { } else {
dueDate.text = DateUtilities.getRelativeDateTime(activity, date, locale.locale, FormatStyle.FULL) dueDate.text = DateUtilities.getRelativeDateTime(activity, date, locale.locale, FormatStyle.FULL)
setTextColor( setTextColor(if (hasDueTime(date)) {
if (hasDueTime(date)) DateTimeUtils.newDateTime(date).isBeforeNow else DateTimeUtils.newDateTime(date).endOfDay().isBeforeNow) DateTimeUtils.newDateTime(date).isBeforeNow
} else {
DateTimeUtils.newDateTime(date).endOfDay().isBeforeNow
})
} }
} }
@ -118,13 +90,12 @@ class DeadlineControlSet : TaskEditControlFragment() {
} }
interface DueDateChangeListener { interface DueDateChangeListener {
fun dueDateChanged(dateTime: Long) fun dueDateChanged()
} }
companion object { companion object {
const val TAG = R.string.TEA_ctrl_when_pref const val TAG = R.string.TEA_ctrl_when_pref
private const val REQUEST_DATE = 504 private const val REQUEST_DATE = 504
private const val EXTRA_DATE = "extra_date"
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker" private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
} }
} }

@ -4,10 +4,8 @@ import android.os.Bundle
import android.widget.EditText import android.widget.EditText
import butterknife.BindView import butterknife.BindView
import butterknife.OnTextChanged import butterknife.OnTextChanged
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.dialogs.Linkify import org.tasks.dialogs.Linkify
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@ -20,53 +18,25 @@ class DescriptionControlSet : TaskEditControlFragment() {
@BindView(R.id.notes) @BindView(R.id.notes)
lateinit var editText: EditText lateinit var editText: EditText
private var description: String? = null
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
description = if (savedInstanceState == null) { viewModel.description?.let(editText::setTextKeepState)
stripCarriageReturns(task.notes)
} else {
savedInstanceState.getString(EXTRA_DESCRIPTION)
}
if (!isNullOrEmpty(description)) {
editText.setTextKeepState(description)
}
if (preferences.getBoolean(R.string.p_linkify_task_edit, false)) { if (preferences.getBoolean(R.string.p_linkify_task_edit, false)) {
linkify.linkify(editText) linkify.linkify(editText)
} }
} }
override fun onSaveInstanceState(outState: Bundle) { override val layout = R.layout.control_set_description
super.onSaveInstanceState(outState)
outState.putString(EXTRA_DESCRIPTION, description)
}
override val layout: Int
get() = R.layout.control_set_description
override val icon: Int override val icon = R.drawable.ic_outline_notes_24px
get() = R.drawable.ic_outline_notes_24px
override fun controlId() = TAG override fun controlId() = TAG
@OnTextChanged(R.id.notes) @OnTextChanged(R.id.notes)
fun textChanged(text: CharSequence) { fun textChanged(text: CharSequence) {
description = text.toString().trim { it <= ' ' } viewModel.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)
} }
companion object { companion object {
const val TAG = R.string.TEA_ctrl_notes_pref 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 android.os.Bundle
import butterknife.BindView import butterknife.BindView
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
@ -21,11 +19,8 @@ class ListFragment : TaskEditControlFragment() {
@BindView(R.id.chip_group) @BindView(R.id.chip_group)
lateinit var chipGroup: ChipGroup lateinit var chipGroup: ChipGroup
@Inject lateinit var taskMover: TaskMover
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
private var originalList: Filter? = null
private lateinit var selectedList: Filter
private lateinit var callback: OnListChanged private lateinit var callback: OnListChanged
interface OnListChanged { interface OnListChanged {
@ -38,80 +33,52 @@ class ListFragment : TaskEditControlFragment() {
} }
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
if (savedInstanceState == null) { refreshView()
originalList = requireArguments().getParcelable(TaskEditFragment.EXTRA_LIST)!!
setSelected(originalList!!)
} else {
originalList = savedInstanceState.getParcelable(EXTRA_ORIGINAL_LIST)
setSelected(savedInstanceState.getParcelable(EXTRA_SELECTED_LIST)!!)
}
} }
private fun setSelected(filter: Filter) { private fun setSelected(filter: Filter) {
selectedList = filter viewModel.selectedList = filter
refreshView() refreshView()
callback.onListChanged(filter) callback.onListChanged(filter)
} }
override fun onSaveInstanceState(outState: Bundle) { override val layout = R.layout.control_set_remote_list
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 icon: Int override val icon = R.drawable.ic_list_24px
get() = R.drawable.ic_list_24px
override fun controlId() = TAG override fun controlId() = TAG
override fun onRowClick() = openPicker() override fun onRowClick() = openPicker()
override val isClickable: Boolean override val isClickable = true
get() = true
private fun openPicker() = 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) .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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SELECT_LIST) { if (requestCode == REQUEST_CODE_SELECT_LIST) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
data?.getParcelableExtra<Filter>(ListPicker.EXTRA_SELECTED_FILTER)?.let { data?.getParcelableExtra<Filter>(ListPicker.EXTRA_SELECTED_FILTER)?.let {
setList(it) if (it is GtasksFilter || it is CaldavFilter) {
} setSelected(it)
}
} else { } else {
super.onActivityResult(requestCode, resultCode, data) throw RuntimeException("Unhandled filter type")
}
} }
} }
private fun setList(list: Filter) {
if (list is GtasksFilter || list is CaldavFilter) {
setSelected(list)
} else { } else {
throw RuntimeException("Unhandled filter type") super.onActivityResult(requestCode, resultCode, data)
} }
} }
private fun refreshView() { private fun refreshView() {
chipGroup.removeAllViews() 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() } chip.setOnClickListener { openPicker() }
chipGroup.addView(chip) chipGroup.addView(chip)
} }
@ -119,8 +86,6 @@ class ListFragment : TaskEditControlFragment() {
companion object { companion object {
const val TAG = R.string.TEA_ctrl_google_task_list 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 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 private const val REQUEST_CODE_SELECT_LIST = 10101
} }
} }

@ -14,21 +14,15 @@ import android.widget.TextView
import androidx.core.util.Pair import androidx.core.util.Pair
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick 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 dagger.hilt.android.AndroidEntryPoint
import org.tasks.PermissionUtil.verifyPermissions import org.tasks.PermissionUtil.verifyPermissions
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.Geofence import org.tasks.data.Geofence
import org.tasks.data.Location import org.tasks.data.Location
import org.tasks.data.LocationDao
import org.tasks.data.Place import org.tasks.data.Place
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.GeofenceDialog import org.tasks.dialogs.GeofenceDialog
import org.tasks.location.GeofenceApi
import org.tasks.location.LocationPickerActivity import org.tasks.location.LocationPickerActivity
import org.tasks.preferences.* import org.tasks.preferences.*
import java.util.* import java.util.*
@ -38,8 +32,6 @@ import javax.inject.Inject
class LocationControlSet : TaskEditControlFragment() { class LocationControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var geofenceApi: GeofenceApi
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var device: Device @Inject lateinit var device: Device
@Inject lateinit var permissionRequestor: FragmentPermissionRequestor @Inject lateinit var permissionRequestor: FragmentPermissionRequestor
@Inject lateinit var permissionChecker: PermissionChecker @Inject lateinit var permissionChecker: PermissionChecker
@ -53,32 +45,19 @@ class LocationControlSet : TaskEditControlFragment() {
@BindView(R.id.geofence_options) @BindView(R.id.geofence_options)
lateinit var geofenceOptions: ImageView 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() { override fun onResume() {
super.onResume() super.onResume()
updateUi() updateUi()
} }
private fun setLocation(location: Location?) { private fun setLocation(location: Location?) {
this.location = location viewModel.selectedLocation = location
updateUi() updateUi()
} }
private fun updateUi() { private fun updateUi() {
val location = viewModel.selectedLocation
if (location == null) { if (location == null) {
locationName.text = "" locationName.text = ""
geofenceOptions.visibility = View.GONE geofenceOptions.visibility = View.GONE
@ -87,9 +66,9 @@ class LocationControlSet : TaskEditControlFragment() {
geofenceOptions.visibility = if (device.supportsGeofences()) View.VISIBLE else View.GONE geofenceOptions.visibility = if (device.supportsGeofences()) View.VISIBLE else View.GONE
geofenceOptions.setImageResource( geofenceOptions.setImageResource(
if (permissionChecker.canAccessLocation() if (permissionChecker.canAccessLocation()
&& (location!!.isArrival || location!!.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px) && (location.isArrival || location.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px)
val name = location!!.displayName val name = location.displayName
val address = location!!.displayAddress val address = location.displayAddress
if (!isNullOrEmpty(address) && address != name) { if (!isNullOrEmpty(address) && address != name) {
locationAddress.text = address locationAddress.text = address
locationAddress.visibility = View.VISIBLE locationAddress.visibility = View.VISIBLE
@ -109,22 +88,23 @@ class LocationControlSet : TaskEditControlFragment() {
} }
override fun onRowClick() { override fun onRowClick() {
val location = viewModel.selectedLocation
if (location == null) { if (location == null) {
chooseLocation() chooseLocation()
} else { } else {
val options: MutableList<Pair<Int, () -> Unit>> = ArrayList() val options: MutableList<Pair<Int, () -> Unit>> = ArrayList()
options.add(Pair.create(R.string.open_map, { location!!.open(activity) })) options.add(Pair.create(R.string.open_map, { location.open(activity) }))
if (!isNullOrEmpty(location!!.phone)) { if (!isNullOrEmpty(location.phone)) {
options.add(Pair.create(R.string.action_call, { call() })) 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.visit_website, { openWebsite() }))
} }
options.add(Pair.create(R.string.choose_new_location, { chooseLocation() })) options.add(Pair.create(R.string.choose_new_location, { chooseLocation() }))
options.add(Pair.create(R.string.delete, { setLocation(null) })) options.add(Pair.create(R.string.delete, { setLocation(null) }))
val items = options.map { requireContext().getString(it.first!!) } val items = options.map { requireContext().getString(it.first!!) }
dialogBuilder dialogBuilder
.newDialog(location!!.displayName) .newDialog(location.displayName)
.setItems(items) { _, which: Int -> .setItems(items) { _, which: Int ->
options[which].second!!.invoke() options[which].second!!.invoke()
} }
@ -132,13 +112,10 @@ class LocationControlSet : TaskEditControlFragment() {
} }
} }
override val isClickable: Boolean
get() = true
private fun chooseLocation() { private fun chooseLocation() {
val intent = Intent(activity, LocationPickerActivity::class.java) val intent = Intent(activity, LocationPickerActivity::class.java)
if (location != null) { viewModel.selectedLocation?.let {
intent.putExtra(LocationPickerActivity.EXTRA_PLACE, location!!.place as Parcelable) intent.putExtra(LocationPickerActivity.EXTRA_PLACE, it.place as Parcelable)
} }
startActivityForResult(intent, REQUEST_LOCATION_REMINDER) startActivityForResult(intent, REQUEST_LOCATION_REMINDER)
} }
@ -168,78 +145,40 @@ class LocationControlSet : TaskEditControlFragment() {
} }
private fun showGeofenceOptions() { private fun showGeofenceOptions() {
val dialog = GeofenceDialog.newGeofenceDialog(location) val dialog = GeofenceDialog.newGeofenceDialog(viewModel.selectedLocation)
dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS) dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS)
dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG) dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG)
} }
override val layout: Int override val layout = R.layout.location_row
get() = R.layout.location_row
override val icon: Int override val icon = R.drawable.ic_outline_place_24px
get() = R.drawable.ic_outline_place_24px
override fun controlId() = TAG override fun controlId() = TAG
private fun openWebsite() { override val isClickable = true
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(location!!.url)))
}
private fun call() {
startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + location!!.phone)))
}
override suspend fun hasChanges(task: Task): Boolean { private fun openWebsite() {
if (original == null) { viewModel.selectedLocation?.let {
return location != null startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url)))
}
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
} }
} }
override fun requiresId() = true private fun call() {
viewModel.selectedLocation?.let {
override suspend fun apply(task: Task) { startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.phone)))
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)
}
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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_LOCATION_REMINDER) { if (requestCode == REQUEST_LOCATION_REMINDER) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!! val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!!
val location = viewModel.selectedLocation
val geofence = if (location == null) { val geofence = if (location == null) {
Geofence(place.uid, preferences) Geofence(place.uid, preferences)
} else { } else {
val existing = location!!.geofence val existing = location.geofence
Geofence( Geofence(
place.uid, place.uid,
existing.isArrival, existing.isArrival,
@ -250,7 +189,7 @@ class LocationControlSet : TaskEditControlFragment() {
} }
} else if (requestCode == REQUEST_GEOFENCE_DETAILS) { } else if (requestCode == REQUEST_GEOFENCE_DETAILS) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
location!!.geofence = data!!.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE)!! viewModel.selectedLocation?.geofence = data!!.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE)!!
updateUi() updateUi()
} }
} else { } else {
@ -263,7 +202,5 @@ class LocationControlSet : TaskEditControlFragment() {
private const val REQUEST_LOCATION_REMINDER = 12153 private const val REQUEST_LOCATION_REMINDER = 12153
private const val REQUEST_GEOFENCE_DETAILS = 12154 private const val REQUEST_GEOFENCE_DETAILS = 12154
private const val FRAG_TAG_LOCATION_DIALOG = "location_dialog" 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.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatRadioButton import androidx.appcompat.widget.AppCompatRadioButton
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick import butterknife.OnClick
@ -30,17 +27,13 @@ class PriorityControlSet : TaskEditControlFragment() {
@BindView(R.id.priority_none) @BindView(R.id.priority_none)
lateinit var priorityNone: AppCompatRadioButton 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) @OnClick(R.id.priority_high, R.id.priority_medium, R.id.priority_low, R.id.priority_none)
fun onPriorityChanged() { fun onPriorityChanged() {
priority = getPriority() viewModel.priority = getPriority()
} }
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
priority = savedInstanceState?.getInt(EXTRA_PRIORITY) ?: task.priority when (viewModel.priority) {
when (priority) {
0 -> priorityHigh.isChecked = true 0 -> priorityHigh.isChecked = true
1 -> priorityMedium.isChecked = true 1 -> priorityMedium.isChecked = true
2 -> priorityLow.isChecked = true 2 -> priorityLow.isChecked = true
@ -52,49 +45,26 @@ class PriorityControlSet : TaskEditControlFragment() {
tintRadioButton(priorityNone, 3) tintRadioButton(priorityNone, 3)
} }
override fun onSaveInstanceState(outState: Bundle) { override val layout = R.layout.control_set_priority
super.onSaveInstanceState(outState)
outState.putInt(EXTRA_PRIORITY, priority)
}
override val layout: Int
get() = R.layout.control_set_priority
override val icon: Int override val icon = R.drawable.ic_outline_flag_24px
get() = R.drawable.ic_outline_flag_24px
override fun controlId() = TAG 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) { private fun tintRadioButton(radioButton: AppCompatRadioButton, priority: Int) {
val color = colorProvider.getPriorityColor(priority, true) 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)) radioButton.buttonTintList = ColorStateList(arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)), intArrayOf(color, color))
} }
@Task.Priority @Task.Priority
private fun getPriority(): Int { private fun getPriority() = when {
if (priorityHigh.isChecked) { priorityHigh.isChecked -> Task.Priority.HIGH
return Task.Priority.HIGH priorityMedium.isChecked -> Task.Priority.MEDIUM
} priorityLow.isChecked -> Task.Priority.LOW
if (priorityMedium.isChecked) { else -> Task.Priority.NONE
return Task.Priority.MEDIUM
}
return if (priorityLow.isChecked) {
Task.Priority.LOW
} else {
Task.Priority.NONE
}
} }
companion object { companion object {
const val TAG = R.string.TEA_ctrl_importance_pref 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.content.Intent
import android.graphics.Paint import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -13,6 +14,7 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -24,9 +26,8 @@ import butterknife.OnClick
import com.todoroo.andlib.sql.Criterion import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Join import com.todoroo.andlib.sql.Join
import com.todoroo.andlib.sql.QueryTemplate 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.activity.MainActivity
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
@ -39,19 +40,19 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.data.CaldavDao
import org.tasks.data.* import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.locale.Locale import org.tasks.locale.Locale
import org.tasks.tasklist.SubtaskViewHolder import org.tasks.tasklist.SubtaskViewHolder
import org.tasks.tasklist.SubtasksRecyclerAdapter import org.tasks.tasklist.SubtasksRecyclerAdapter
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks { class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks {
@JvmField
@BindView(R.id.recycler_view) @BindView(R.id.recycler_view)
var recyclerView: RecyclerView? = null lateinit var recyclerView: RecyclerView
@BindView(R.id.new_subtasks) @BindView(R.id.new_subtasks)
lateinit var newSubtaskContainer: LinearLayout lateinit var newSubtaskContainer: LinearLayout
@ -68,104 +69,45 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
@Inject lateinit var checkBoxProvider: CheckBoxProvider @Inject lateinit var checkBoxProvider: CheckBoxProvider
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
private lateinit var viewModel: TaskListViewModel private lateinit var listViewModel: TaskListViewModel
private val refreshReceiver = RefreshReceiver() private val refreshReceiver = RefreshReceiver()
private var remoteList: Filter? = null private var remoteList: Filter? = null
private var googleTask: GoogleTask? = null private var googleTask: GoogleTask? = null
private lateinit var recyclerAdapter: SubtasksRecyclerAdapter private lateinit var recyclerAdapter: SubtasksRecyclerAdapter
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(EXTRA_NEW_SUBTASKS, newSubtasks)
}
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
viewModel = ViewModelProvider(this).get(TaskListViewModel::class.java) listViewModel = ViewModelProvider(this).get(TaskListViewModel::class.java)
if (savedInstanceState != null) { viewModel.newSubtasks.forEach { addSubtask(it) }
for (task in savedInstanceState.getParcelableArrayList<Task>(EXTRA_NEW_SUBTASKS)!!) {
addSubtask(task)
}
}
recyclerAdapter = SubtasksRecyclerAdapter(activity, chipProvider, checkBoxProvider, this) recyclerAdapter = SubtasksRecyclerAdapter(activity, chipProvider, checkBoxProvider, this)
if (task.id > 0) { viewModel.task?.let {
recyclerAdapter.submitList(viewModel.value) if (it.id > 0) {
viewModel.setFilter(Filter("subtasks", getQueryTemplate(task))) recyclerAdapter.submitList(listViewModel.value)
(recyclerView!!.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false listViewModel.setFilter(Filter("subtasks", getQueryTemplate(it)))
recyclerView!!.layoutManager = LinearLayoutManager(activity) (recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView!!.isNestedScrollingEnabled = false recyclerView.layoutManager = LinearLayoutManager(activity)
viewModel.observe(this, Observer { list: List<TaskContainer?>? -> recyclerAdapter.submitList(list) }) recyclerView.isNestedScrollingEnabled = false
recyclerView!!.adapter = recyclerAdapter listViewModel.observe(this, Observer { list: List<TaskContainer?>? -> recyclerAdapter.submitList(list) })
recyclerView.adapter = recyclerAdapter
}
} }
} }
override val layout: Int override val layout = R.layout.control_set_subtasks
get() = R.layout.control_set_subtasks
override val icon: Int override val icon = R.drawable.ic_subdirectory_arrow_right_black_24dp
get() = R.drawable.ic_subdirectory_arrow_right_black_24dp
override fun controlId() = TAG 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() { override fun onResume() {
super.onResume() super.onResume()
localBroadcastManager.registerRefreshReceiver(refreshReceiver) localBroadcastManager.registerRefreshReceiver(refreshReceiver)
lifecycleScope.launch { lifecycleScope.launch {
googleTask = googleTaskDao.getByTaskId(task.id) viewModel.task?.let {
googleTask = googleTaskDao.getByTaskId(it.id)
updateUI() updateUI()
} }
} }
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
@ -178,7 +120,9 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
toaster.longToast(R.string.subtasks_multilevel_google_task) toaster.longToast(R.string.subtasks_multilevel_google_task)
return return
} }
val editText = addSubtask(taskCreator.createWithValues("")) val task = taskCreator.createWithValues("")
viewModel.newSubtasks.add(task)
val editText = addSubtask(task)
editText.requestFocus() editText.requestFocus()
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
@ -195,6 +139,9 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
editText.maxLines = Int.MAX_VALUE editText.maxLines = Int.MAX_VALUE
editText.isFocusable = true editText.isFocusable = true
editText.isEnabled = true editText.isEnabled = true
editText.addTextChangedListener { text: Editable? ->
task.title = text?.toString()
}
editText.setOnEditorActionListener { _, actionId: Int, _ -> editText.setOnEditorActionListener { _, actionId: Int, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) { if (actionId == EditorInfo.IME_ACTION_NEXT) {
if (editText.text.isNotEmpty()) { if (editText.text.isNotEmpty()) {
@ -214,6 +161,7 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
private fun updateCompleteBox(task: Task, completeBox: CheckableImageView, editText: EditText) { private fun updateCompleteBox(task: Task, completeBox: CheckableImageView, editText: EditText) {
val isComplete = completeBox.isChecked val isComplete = completeBox.isChecked
task.completionDate = if (isComplete) now() else 0
completeBox.setImageDrawable( completeBox.setImageDrawable(
checkBoxProvider.getCheckBox(isComplete, false, task.priority)) checkBoxProvider.getCheckBox(isComplete, false, task.priority))
editText.paintFlags = if (isComplete) { editText.paintFlags = if (isComplete) {
@ -229,10 +177,10 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
private fun updateUI() { private fun updateUI() {
if (isGoogleTaskChild) { if (isGoogleTaskChild) {
recyclerView!!.visibility = View.GONE recyclerView.visibility = View.GONE
newSubtaskContainer.visibility = View.GONE newSubtaskContainer.visibility = View.GONE
} else { } else {
recyclerView!!.visibility = View.VISIBLE recyclerView.visibility = View.VISIBLE
newSubtaskContainer.visibility = View.VISIBLE newSubtaskContainer.visibility = View.VISIBLE
recyclerAdapter.setMultiLevelSubtasksEnabled(remoteList !is GtasksFilter) recyclerAdapter.setMultiLevelSubtasksEnabled(remoteList !is GtasksFilter)
refresh() refresh()
@ -241,13 +189,11 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
fun onRemoteListChanged(filter: Filter?) { fun onRemoteListChanged(filter: Filter?) {
remoteList = filter remoteList = filter
if (recyclerView != null) {
updateUI() updateUI()
} }
}
private fun refresh() { private fun refresh() {
viewModel.invalidate() listViewModel.invalidate()
} }
override fun openSubtask(task: Task) { override fun openSubtask(task: Task) {
@ -273,7 +219,6 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
companion object { companion object {
const val TAG = R.string.TEA_ctrl_subtask_pref const val TAG = R.string.TEA_ctrl_subtask_pref
private const val EXTRA_NEW_SUBTASKS = "extra_new_subtasks"
private fun getQueryTemplate(task: Task): QueryTemplate { private fun getQueryTemplate(task: Task): QueryTemplate {
return QueryTemplate() return QueryTemplate()
.join( .join(

@ -1,6 +1,5 @@
package org.tasks.ui package org.tasks.ui
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -8,21 +7,17 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.ViewModelProviders
import butterknife.ButterKnife import butterknife.ButterKnife
import com.todoroo.astrid.data.Task
import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
abstract class TaskEditControlFragment : Fragment() { abstract class TaskEditControlFragment : Fragment() {
protected lateinit var task: Task lateinit var viewModel: TaskEditViewModel
var isNew = false
private set
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.control_set_template, null) 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) val content = view.findViewById<LinearLayout>(R.id.content)
inflater.inflate(layout, content) inflater.inflate(layout, content)
val icon = view.findViewById<ImageView>(R.id.icon) val icon = view.findViewById<ImageView>(R.id.icon)
@ -32,21 +27,12 @@ abstract class TaskEditControlFragment : Fragment() {
} }
ButterKnife.bind(this, view) ButterKnife.bind(this, view)
lifecycleScope.launch {
createView(savedInstanceState) createView(savedInstanceState)
}
return view return view
} }
protected abstract fun createView(savedInstanceState: Bundle?) protected open 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 onRowClick() {} protected open fun onRowClick() {}
protected open val isClickable: Boolean protected open val isClickable: Boolean
@ -55,18 +41,4 @@ abstract class TaskEditControlFragment : Fragment() {
protected abstract val layout: Int protected abstract val layout: Int
protected abstract val icon: Int protected abstract val icon: Int
abstract fun controlId(): 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