diff --git a/app/src/androidTest/java/org/tasks/ui/DescriptionControlSetTest.kt b/app/src/androidTest/java/org/tasks/ui/DescriptionControlSetTest.kt deleted file mode 100644 index 62682c015..000000000 --- a/app/src/androidTest/java/org/tasks/ui/DescriptionControlSetTest.kt +++ /dev/null @@ -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)) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/ui/editviewmodel/BaseTaskEditViewModelTest.kt b/app/src/androidTest/java/org/tasks/ui/editviewmodel/BaseTaskEditViewModelTest.kt new file mode 100644 index 000000000..b77b02134 --- /dev/null +++ b/app/src/androidTest/java/org/tasks/ui/editviewmodel/BaseTaskEditViewModelTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/ui/editviewmodel/PriorityTests.kt b/app/src/androidTest/java/org/tasks/ui/editviewmodel/PriorityTests.kt new file mode 100644 index 000000000..7e9ea28a3 --- /dev/null +++ b/app/src/androidTest/java/org/tasks/ui/editviewmodel/PriorityTests.kt @@ -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()) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/ui/editviewmodel/ReminderTests.kt b/app/src/androidTest/java/org/tasks/ui/editviewmodel/ReminderTests.kt new file mode 100644 index 000000000..057d0e93c --- /dev/null +++ b/app/src/androidTest/java/org/tasks/ui/editviewmodel/ReminderTests.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/ui/editviewmodel/RepeatTests.kt b/app/src/androidTest/java/org/tasks/ui/editviewmodel/RepeatTests.kt new file mode 100644 index 000000000..48fec28c7 --- /dev/null +++ b/app/src/androidTest/java/org/tasks/ui/editviewmodel/RepeatTests.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/ui/editviewmodel/TaskEditViewModelTest.kt b/app/src/androidTest/java/org/tasks/ui/editviewmodel/TaskEditViewModelTest.kt new file mode 100644 index 000000000..6102802bf --- /dev/null +++ b/app/src/androidTest/java/org/tasks/ui/editviewmodel/TaskEditViewModelTest.kt @@ -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()) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/ui/editviewmodel/TitleTests.kt b/app/src/androidTest/java/org/tasks/ui/editviewmodel/TitleTests.kt new file mode 100644 index 000000000..72a3293a8 --- /dev/null +++ b/app/src/androidTest/java/org/tasks/ui/editviewmodel/TitleTests.kt @@ -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()) + } +} \ No newline at end of file diff --git a/app/src/commonTest/java/org/tasks/makers/TaskMaker.kt b/app/src/commonTest/java/org/tasks/makers/TaskMaker.kt index 1b27353b3..4fb3d5dcb 100644 --- a/app/src/commonTest/java/org/tasks/makers/TaskMaker.kt +++ b/app/src/commonTest/java/org/tasks/makers/TaskMaker.kt @@ -27,8 +27,8 @@ object TaskMaker { val SNOOZE_TIME: Property = newProperty() val RRULE: Property = newProperty() val AFTER_COMPLETE: Property = newProperty() - private val TITLE: Property = newProperty() - private val PRIORITY: Property = newProperty() + val TITLE: Property = newProperty() + val PRIORITY: Property = newProperty() val PARENT: Property = newProperty() val UUID: Property = newProperty() diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt index c649bd9c8..72e6a6e19 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -20,7 +20,6 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment -import com.todoroo.astrid.activity.TaskEditFragment.TaskEditFragmentCallbackHandler import com.todoroo.astrid.activity.TaskListFragment.TaskListFragmentCallbackHandler import com.todoroo.astrid.api.Filter import com.todoroo.astrid.dao.TaskDao @@ -28,8 +27,9 @@ import com.todoroo.astrid.data.Task import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.timers.TimerControlSet.TimerControlSetCallback import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.async +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.tasks.LocalBroadcastManager import org.tasks.R import org.tasks.activities.TagSettingsActivity @@ -60,7 +60,7 @@ import org.tasks.ui.NavigationDrawerFragment import javax.inject.Inject @AndroidEntryPoint -class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandler, OnListChanged, TimerControlSetCallback, DueDateChangeListener, TaskEditFragmentCallbackHandler, CommentBarFragmentCallback, SortDialogCallback { +class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandler, OnListChanged, TimerControlSetCallback, DueDateChangeListener, CommentBarFragmentCallback, SortDialogCallback { @Inject lateinit var preferences: Preferences @Inject lateinit var repeatConfirmationReceiver: RepeatConfirmationReceiver @Inject lateinit var defaultFilterProvider: DefaultFilterProvider @@ -175,7 +175,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl val loadFilter = intent.hasExtra(LOAD_FILTER) val tef = taskEditFragment if (tef != null && (openFilter || loadFilter)) { - tef.save() + lifecycleScope.launch(NonCancellable) { + tef.save() + } } if (loadFilter || !openFilter && filter == null) { lifecycleScope.launch { @@ -305,15 +307,15 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl if (task == null) { return } - val taskEditFragment = taskEditFragment - taskEditFragment?.save() - clearUi() lifecycleScope.launch { - val list = async { defaultFilterProvider.getList(task) } - val location = async { locationDao.getLocation(task, preferences) } - val tags = async { tagDataDao.getTags(task) } - val fragment = newTaskEditFragment( - task, filterColor, list.await(), location.await(), tags.await()) + taskEditFragment?.let { + it.editViewModel.cleared.removeObservers(this@MainActivity) + withContext(NonCancellable) { + it.save() + } + } + clearUi() + val fragment = newTaskEditFragment(task, filterColor) supportFragmentManager.beginTransaction() .replace(R.id.detail, fragment, TaskEditFragment.TAG_TASKEDIT_FRAGMENT) .addToBackStack(TaskEditFragment.TAG_TASKEDIT_FRAGMENT) @@ -332,14 +334,15 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl navigationDrawer.closeDrawer() return } - val taskEditFragment = taskEditFragment - if (taskEditFragment != null) { + taskEditFragment?.let { if (preferences.backButtonSavesTask()) { - taskEditFragment.save() + lifecycleScope.launch(NonCancellable) { + it.save() + } } else { - taskEditFragment.discardButtonClick() + it.discardButtonClick() } - return + return@onBackPressed } if (taskListFragment?.collapseSearchView() == true) { return @@ -364,7 +367,7 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl private val isSinglePaneLayout: Boolean get() = !resources.getBoolean(R.bool.two_pane_layout) - override fun removeTaskEditFragment() { + fun removeTaskEditFragment() { supportFragmentManager .popBackStackImmediate( TaskEditFragment.TAG_TASKEDIT_FRAGMENT, FragmentManager.POP_BACK_STACK_INCLUSIVE) @@ -413,8 +416,8 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl actionMode = null } - override fun dueDateChanged(dateTime: Long) { - taskEditFragment!!.onDueDateChanged(dateTime) + override fun dueDateChanged() { + taskEditFragment!!.onDueDateChanged() } override fun onListChanged(filter: Filter?) { diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt index 341a6e74e..e2a906d33 100755 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt @@ -20,32 +20,30 @@ import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.DateUtilities -import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.Filter -import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.data.Task import com.todoroo.astrid.notes.CommentsController import com.todoroo.astrid.repeats.RepeatControlSet -import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.timers.TimerPlugin import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.tasks.BuildConfig +import org.tasks.Event import org.tasks.R import org.tasks.Strings.isNullOrEmpty import org.tasks.analytics.Firebase -import org.tasks.data.Location -import org.tasks.data.TagData import org.tasks.data.UserActivity import org.tasks.data.UserActivityDao import org.tasks.databinding.FragmentTaskEditBinding @@ -59,6 +57,7 @@ import org.tasks.preferences.Preferences import org.tasks.themes.ThemeColor import org.tasks.ui.SubtaskControlSet import org.tasks.ui.TaskEditControlFragment +import org.tasks.ui.TaskEditViewModel import javax.inject.Inject import kotlin.math.abs @@ -66,7 +65,6 @@ import kotlin.math.abs class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { @Inject lateinit var taskDao: TaskDao @Inject lateinit var userActivityDao: UserActivityDao - @Inject lateinit var taskDeleter: TaskDeleter @Inject lateinit var notificationManager: NotificationManager @Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var context: Activity @@ -76,33 +74,34 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { @Inject lateinit var firebase: Firebase @Inject lateinit var timerPlugin: TimerPlugin @Inject lateinit var linkify: Linkify - - lateinit var model: Task + + val editViewModel: TaskEditViewModel by viewModels() lateinit var binding: FragmentTaskEditBinding - private var callback: TaskEditFragmentCallbackHandler? = null private var showKeyboard = false - private var completed = false - - override fun onAttach(activity: Activity) { - super.onAttach(activity) - callback = activity as TaskEditFragmentCallbackHandler - } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putBoolean(EXTRA_COMPLETED, completed) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + editViewModel.setup(requireArguments().getParcelable(EXTRA_TASK)!!) + val activity = requireActivity() as MainActivity + editViewModel.cleared.observe(activity, Observer> { + activity.removeTaskEditFragment() + }) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = FragmentTaskEditBinding.inflate(inflater) val view: View = binding.root - val arguments = requireArguments() - model = arguments.getParcelable(EXTRA_TASK)!! - val themeColor: ThemeColor = arguments.getParcelable(EXTRA_THEME)!! + val model = editViewModel.task!! + val themeColor: ThemeColor = requireArguments().getParcelable(EXTRA_THEME)!! val toolbar = binding.toolbar toolbar.navigationIcon = context.getDrawable(R.drawable.ic_outline_save_24px) - toolbar.setNavigationOnClickListener { save() } + toolbar.setNavigationOnClickListener { + lifecycleScope.launch(NonCancellable) { + save() + } + } val backButtonSavesTask = preferences.backButtonSavesTask() toolbar.inflateMenu(R.menu.menu_task_edit_fragment) val menu = toolbar.menu @@ -116,9 +115,6 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { if (model.isNew) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER) if (savedInstanceState == null) { showKeyboard = model.isNew && isNullOrEmpty(model.title) - completed = model.isCompleted - } else { - completed = savedInstanceState.getBoolean(EXTRA_COMPLETED) } val params = binding.appbarlayout.layoutParams as CoordinatorLayout.LayoutParams params.behavior = AppBarLayout.Behavior() @@ -136,20 +132,25 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { title.setTextColor(themeColor.colorOnPrimary) title.setHintTextColor(themeColor.hintOnPrimary) title.maxLines = 5 + title.addTextChangedListener { text -> + editViewModel.title = text.toString().trim { it <= ' ' } + } if (model.isNew || preferences.getBoolean(R.string.p_hide_check_button, false)) { binding.fab.visibility = View.INVISIBLE - } else if (completed) { + } else if (editViewModel.completed!!) { title.paintFlags = title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG binding.fab.setImageResource(R.drawable.ic_outline_check_box_outline_blank_24px) } binding.fab.setOnClickListener { - if (completed) { - completed = false + if (editViewModel.completed!!) { + editViewModel.completed = false title.paintFlags = title.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() binding.fab.setImageResource(R.drawable.ic_outline_check_box_24px) } else { - completed = true - save() + editViewModel.completed = true + lifecycleScope.launch(NonCancellable) { + save() + } } } if (AndroidUtilities.atLeastQ()) { @@ -176,7 +177,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { commentsController.reloadView() val fragmentManager = childFragmentManager val taskEditControlFragments = - taskEditControlSetFragmentManager.getOrCreateFragments(fragmentManager, model, arguments) + taskEditControlSetFragmentManager.getOrCreateFragments(fragmentManager) val visibleSize = taskEditControlSetFragmentManager.visibleSize val fragmentTransaction = fragmentManager.beginTransaction() for (i in taskEditControlFragments.indices) { @@ -199,6 +200,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { override fun onResume() { super.onResume() + if (showKeyboard) { binding.title.requestFocus() val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager @@ -219,6 +221,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { } fun stopTimer(): Task { + val model = editViewModel.task!! timerPlugin.stopTimer(model) val elapsedTime = DateUtils.formatElapsedTime(model.elapsedSeconds.toLong()) addComment(String.format( @@ -232,6 +235,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { } fun startTimer(): Task { + val model = editViewModel.task!! timerPlugin.startTimer(model) addComment(String.format( "%s %s", @@ -242,39 +246,21 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { } /** Save task model from values in UI components */ - fun save() { - val fragments = taskEditControlSetFragmentManager.getFragmentsInPersistOrder(childFragmentManager) - lifecycleScope.launch(NonCancellable) { - if (hasChanges(fragments)) { - val isNewTask = model.isNew - val taskListFragment = (activity as MainActivity?)!!.taskListFragment - val title = title - model.title = if (isNullOrEmpty(title)) getString(R.string.no_title) else title - if (completed != model.isCompleted) { - model.completionDate = if (completed) DateUtilities.now() else 0 - } - val partition = fragments.partition { it.requiresId() } - partition.second.forEach { it.apply(model) } - if (isNewTask) { - taskDao.createNew(model) - } - partition.first.forEach { it.apply(model) } - taskDao.save(model, null) - if (isNewTask) { - taskListFragment!!.onTaskCreated(model.uuid) - if (!isNullOrEmpty(model.calendarURI)) { - taskListFragment.makeSnackbar(R.string.calendar_event_created, model.title) - .setAction(R.string.action_open) { - val uri = model.calendarURI - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) - taskListFragment.startActivity(intent) - } - .show() - } + suspend fun save() { + val saved = editViewModel.save() + if (saved && editViewModel.isNew) { + (activity as MainActivity?)?.taskListFragment?.let { taskListFragment -> + val model = editViewModel.task!! + taskListFragment.onTaskCreated(model.uuid) + if (!isNullOrEmpty(model.calendarURI)) { + taskListFragment.makeSnackbar(R.string.calendar_event_created, model.title) + .setAction(R.string.action_open) { + val uri = model.calendarURI + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) + taskListFragment.startActivity(intent) + } + .show() } - callback!!.removeTaskEditFragment() - } else { - discard() } } } @@ -284,68 +270,37 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { * =============================================== model reading / saving * ====================================================================== */ - private val repeatControlSet: RepeatControlSet - get() = getFragment(RepeatControlSet.TAG)!! + private val repeatControlSet: RepeatControlSet? + get() = getFragment(RepeatControlSet.TAG) - private val subtaskControlSet: SubtaskControlSet - get() = getFragment(SubtaskControlSet.TAG)!! + private val subtaskControlSet: SubtaskControlSet? + get() = getFragment(SubtaskControlSet.TAG) private fun getFragment(tag: Int): T? { return childFragmentManager.findFragmentByTag(getString(tag)) as T? } - private val title: String - get() = binding.title.text.toString().trim { it <= ' ' } - - private suspend fun hasChanges(fragments: List): Boolean { - val newTitle = title - if (newTitle != model.title - || !model.isNew && completed != model.isCompleted - || model.isNew && !isNullOrEmpty(newTitle)) { - return true - } - try { - return fragments.any { it.hasChanges(model) } - } catch (e: Exception) { - firebase.reportException(e) - } - return false - } - - /* + /* * ====================================================================== * ======================================================= event handlers * ====================================================================== */ fun discardButtonClick() { - val fragments = taskEditControlSetFragmentManager.getFragmentsInPersistOrder(childFragmentManager) - lifecycleScope.launch { - if (hasChanges(fragments)) { - dialogBuilder - .newDialog(R.string.discard_confirmation) - .setPositiveButton(R.string.keep_editing, null) - .setNegativeButton(R.string.discard) { _, _ -> discard() } - .show() - } else { - discard() - } - } - } - - fun discard() { - if (model.isNew) { - timerPlugin.stopTimer(model) - } - callback!!.removeTaskEditFragment() + if (editViewModel.hasChanges()) { + dialogBuilder + .newDialog(R.string.discard_confirmation) + .setPositiveButton(R.string.keep_editing, null) + .setNegativeButton(R.string.discard) { _, _ -> editViewModel.discard() } + .show() + } else { + editViewModel.discard() + } } private fun deleteButtonClick() { dialogBuilder .newDialog(R.string.DLG_delete_this_task_question) - .setPositiveButton(android.R.string.ok) { _, _ -> - taskDeleter.markDeleted(model) - callback!!.removeTaskEditFragment() - } + .setPositiveButton(android.R.string.ok) { _, _ -> editViewModel.delete() } .setNegativeButton(android.R.string.cancel, null) .show() } @@ -355,17 +310,16 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { * ========================================== UI component helper classes * ====================================================================== */ - fun onDueDateChanged(dueDate: Long) { - val repeatControlSet: RepeatControlSet? = repeatControlSet - repeatControlSet?.onDueDateChanged(dueDate) + fun onDueDateChanged() { + repeatControlSet?.onDueDateChanged() } fun onRemoteListChanged(filter: Filter?) { - val subtaskControlSet: SubtaskControlSet? = subtaskControlSet subtaskControlSet?.onRemoteListChanged(filter) } fun addComment(message: String?, picture: Uri?) { + val model = editViewModel.task!! val userActivity = UserActivity() if (picture != null) { val output = FileHelper.copyToUri(context, preferences.attachmentsDirectory, picture) @@ -382,35 +336,16 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { } } - interface TaskEditFragmentCallbackHandler { - fun removeTaskEditFragment() - } - companion object { const val TAG_TASKEDIT_FRAGMENT = "taskedit_fragment" private const val EXTRA_TASK = "extra_task" private const val EXTRA_THEME = "extra_theme" - const val EXTRA_LIST = "extra_list" - const val EXTRA_PLACE = "extra_place" - const val EXTRA_TAGS = "extra_tags" - private const val EXTRA_COMPLETED = "extra_completed" - fun newTaskEditFragment( - task: Task, - themeColor: ThemeColor?, - filter: Filter, - place: Location?, - tags: ArrayList): TaskEditFragment { - if (BuildConfig.DEBUG) { - require(filter is GtasksFilter || filter is CaldavFilter) - } + fun newTaskEditFragment(task: Task, themeColor: ThemeColor?): TaskEditFragment { val taskEditFragment = TaskEditFragment() val arguments = Bundle() arguments.putParcelable(EXTRA_TASK, task) arguments.putParcelable(EXTRA_THEME, themeColor) - arguments.putParcelable(EXTRA_LIST, filter) - arguments.putParcelable(EXTRA_PLACE, place) - arguments.putParcelableArrayList(EXTRA_TAGS, tags) taskEditFragment.arguments = arguments return taskEditFragment } diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt index bfdd7e56f..b608a9c37 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -502,11 +502,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL } private fun onTaskDelete(task: Task) { - val activity = activity as MainActivity? - if (activity != null) { - val tef = activity.taskEditFragment - if (tef != null && task.id == tef.model.id) { - tef.discard() + (activity as MainActivity?)?.taskEditFragment?.let { + if (task.id == it.editViewModel.task?.id) { + it.editViewModel.discard() } } timerPlugin.stopTimer(task) diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt index 4e08a941f..3f2fa2d74 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt @@ -226,6 +226,9 @@ SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtas if (!task.insignificantChange(original)) { task.modificationDate = DateUtilities.now() } + if (task.dueDate != original?.dueDate) { + task.reminderSnooze = 0 + } if (update(task) == 1) { workManager.afterSave(task, original) } @@ -245,6 +248,9 @@ SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtas if (Task.isUuidEmpty(task.remoteId)) { task.remoteId = UUIDHelper.newUUID() } + if (BuildConfig.DEBUG) { + require(task.remoteId?.isNotBlank() == true && task.remoteId != "0") + } val insert = insert(task) task.id = insert } diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.kt b/app/src/main/java/com/todoroo/astrid/data/Task.kt index 5e28155a4..ffb5b71d4 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.kt +++ b/app/src/main/java/com/todoroo/astrid/data/Task.kt @@ -11,7 +11,6 @@ import com.todoroo.andlib.data.Table import com.todoroo.andlib.sql.Field import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.dao.TaskDao -import com.todoroo.astrid.dao.TaskDaoBlocking import org.tasks.Strings import org.tasks.backup.XmlReader import org.tasks.data.Tag @@ -233,11 +232,11 @@ class Task : Parcelable { return dueDate < compareTo && !isCompleted } - fun repeatAfterCompletion(): Boolean = recurrence?.contains("FROM=COMPLETION") ?: false + fun repeatAfterCompletion(): Boolean = recurrence.isRepeatAfterCompletion() fun sanitizedRecurrence(): String? = getRecurrenceWithoutFrom()?.replace("BYDAY=;".toRegex(), "") // $NON-NLS-1$//$NON-NLS-2$ - fun getRecurrenceWithoutFrom(): String? = recurrence?.replace(";?FROM=[^;]*".toRegex(), "") + fun getRecurrenceWithoutFrom(): String? = recurrence.withoutFrom() fun setDueDateAdjustingHideUntil(newDueDate: Long) { if (dueDate > 0) { @@ -612,5 +611,9 @@ class Task : Parcelable { @JvmStatic fun isUuidEmpty(uuid: String?): Boolean { return NO_UUID == uuid || Strings.isNullOrEmpty(uuid) } + + fun String?.isRepeatAfterCompletion() = this?.contains("FROM=COMPLETION") ?: false + + fun String?.withoutFrom(): String? = this?.replace(";?FROM=[^;]*".toRegex(), "") } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt b/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt index 90fb5c152..ab353ab00 100644 --- a/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt @@ -16,7 +16,6 @@ import android.widget.TextView import androidx.lifecycle.lifecycleScope import butterknife.BindView import butterknife.OnClick -import com.todoroo.astrid.data.Task import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch @@ -45,10 +44,8 @@ class FilesControlSet : TaskEditControlFragment() { @BindView(R.id.add_attachment) lateinit var addAttachment: TextView - private var taskUuid: String? = null - override fun createView(savedInstanceState: Bundle?) { - taskUuid = task.uuid + val task = viewModel.task!! if (savedInstanceState == null) { if (task.hasTransitory(TaskAttachment.KEY)) { for (uri in (task.getTransitory>(TaskAttachment.KEY))!!) { @@ -69,16 +66,12 @@ class FilesControlSet : TaskEditControlFragment() { AddAttachmentDialog.newAddAttachmentDialog(this).show(parentFragmentManager, FRAG_TAG_ADD_ATTACHMENT_DIALOG) } - override val layout: Int - get() = R.layout.control_set_files + override val layout = R.layout.control_set_files - override val icon: Int - get() = R.drawable.ic_outline_attachment_24px + override val icon = R.drawable.ic_outline_attachment_24px override fun controlId() = TAG - override suspend fun apply(task: Task) {} - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == AddAttachmentDialog.REQUEST_CAMERA || requestCode == AddAttachmentDialog.REQUEST_AUDIO) { if (resultCode == Activity.RESULT_OK) { @@ -143,7 +136,10 @@ class FilesControlSet : TaskEditControlFragment() { } private fun newAttachment(output: Uri) { - val attachment = TaskAttachment(taskUuid!!, output, FileHelper.getFilename(context, output)!!) + val attachment = TaskAttachment( + viewModel.task!!.uuid, + output, + FileHelper.getFilename(context, output)!!) lifecycleScope.launch { taskAttachmentDao.createNew(attachment) addAttachment(attachment) diff --git a/app/src/main/java/com/todoroo/astrid/gcal/GCalHelper.java b/app/src/main/java/com/todoroo/astrid/gcal/GCalHelper.java index 496cf7bcc..b75d77a35 100644 --- a/app/src/main/java/com/todoroo/astrid/gcal/GCalHelper.java +++ b/app/src/main/java/com/todoroo/astrid/gcal/GCalHelper.java @@ -83,7 +83,9 @@ public class GCalHelper { } } - public Uri createTaskEvent(Task task, ContentValues values) { + public Uri createTaskEvent(Task task, String calendarId) { + ContentValues values = new ContentValues(); + values.put(CalendarContract.Events.CALENDAR_ID, calendarId); return createTaskEvent(task, values, true); } @@ -127,6 +129,19 @@ public class GCalHelper { return null; } + public void updateEvent(String uri, Task task) { + try { + ContentValues updateValues = new ContentValues(); + // check if we need to update the item + updateValues.put(CalendarContract.Events.TITLE, task.getTitle()); + updateValues.put(CalendarContract.Events.DESCRIPTION, task.getNotes()); + createStartAndEndDate(task, updateValues); + cr.update(Uri.parse(uri), updateValues, null, null); + } catch (Exception e) { + Timber.e(e, "Failed to update calendar: %s [%s]", uri, task); + } + } + public void rescheduleRepeatingTask(Task task) { String taskUri = getTaskEventUri(task); if (isNullOrEmpty(taskUri)) { diff --git a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt index f2bf6c965..e2a6161a6 100644 --- a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt @@ -18,20 +18,17 @@ import butterknife.OnItemSelected import com.google.ical.values.Frequency import com.google.ical.values.RRule import com.google.ical.values.WeekdayNum -import com.todoroo.astrid.data.Task import dagger.hilt.android.AndroidEntryPoint import org.tasks.R -import org.tasks.Strings.isNullOrEmpty import org.tasks.analytics.Firebase import org.tasks.dialogs.DialogBuilder import org.tasks.repeats.BasicRecurrenceDialog import org.tasks.repeats.RepeatRuleToString import org.tasks.themes.Theme import org.tasks.time.DateTime -import org.tasks.time.DateTimeUtils +import org.tasks.time.DateTimeUtils.currentTimeMillis import org.tasks.ui.HiddenTopArrayAdapter import org.tasks.ui.TaskEditControlFragment -import java.text.ParseException import java.util.* import javax.inject.Inject @@ -59,65 +56,33 @@ class RepeatControlSet : TaskEditControlFragment() { @BindView(R.id.repeatTypeContainer) lateinit var repeatTypeContainer: LinearLayout - private var rrule: RRule? = null private lateinit var typeAdapter: HiddenTopArrayAdapter - private var dueDate: Long = 0 - private var repeatAfterCompletion = false - + fun onSelected(rrule: RRule?) { - this.rrule = rrule + viewModel.rrule = rrule refreshDisplayView() } - fun onDueDateChanged(dueDate: Long) { - this.dueDate = if (dueDate > 0) dueDate else DateTimeUtils.currentTimeMillis() - if (rrule != null && rrule!!.freq == Frequency.MONTHLY && rrule!!.byDay.isNotEmpty()) { - val weekdayNum = rrule!!.byDay[0] - val dateTime = DateTime(this.dueDate) - val num: Int - val dayOfWeekInMonth = dateTime.dayOfWeekInMonth - num = if (weekdayNum.num == -1 || dayOfWeekInMonth == 5) { - if (dayOfWeekInMonth == dateTime.maxDayOfWeekInMonth) -1 else dayOfWeekInMonth - } else { - dayOfWeekInMonth + fun onDueDateChanged() { + viewModel.rrule?.let { + if (it.freq == Frequency.MONTHLY && it.byDay.isNotEmpty()) { + val weekdayNum = it.byDay[0] + val dateTime = DateTime(this.dueDate) + val num: Int + val dayOfWeekInMonth = dateTime.dayOfWeekInMonth + num = if (weekdayNum.num == -1 || dayOfWeekInMonth == 5) { + if (dayOfWeekInMonth == dateTime.maxDayOfWeekInMonth) -1 else dayOfWeekInMonth + } else { + dayOfWeekInMonth + } + it.byDay = listOf((WeekdayNum(num, dateTime.weekday))) + viewModel.rrule = it + refreshDisplayView() } - rrule!!.byDay = listOf((WeekdayNum(num, dateTime.weekday))) - refreshDisplayView() } } override fun createView(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - repeatAfterCompletion = task.repeatAfterCompletion() - dueDate = task.dueDate - if (dueDate <= 0) { - dueDate = DateTimeUtils.currentTimeMillis() - } - val recurrenceWithoutFrom = task.getRecurrenceWithoutFrom() - if (isNullOrEmpty(recurrenceWithoutFrom)) { - rrule = null - } else { - try { - rrule = RRule(recurrenceWithoutFrom) - rrule!!.until = DateTime(task.repeatUntil).toDateValue() - } catch (e: ParseException) { - rrule = null - } - } - } else { - val recurrence = savedInstanceState.getString(EXTRA_RECURRENCE) - dueDate = savedInstanceState.getLong(EXTRA_DUE_DATE) - rrule = if (isNullOrEmpty(recurrence)) { - null - } else { - try { - RRule(recurrence) - } catch (e: ParseException) { - null - } - } - repeatAfterCompletion = savedInstanceState.getBoolean(EXTRA_REPEAT_AFTER_COMPLETION) - } repeatTypes.add("") repeatTypes.addAll(listOf(*resources.getStringArray(R.array.repeat_type))) typeAdapter = object : HiddenTopArrayAdapter(activity, 0, repeatTypes) { @@ -136,76 +101,42 @@ class RepeatControlSet : TaskEditControlFragment() { drawable.setTint(activity.getColor(R.color.text_primary)) typeSpinner.setBackgroundDrawable(drawable) typeSpinner.adapter = typeAdapter - typeSpinner.setSelection(if (repeatAfterCompletion) TYPE_COMPLETION_DATE else TYPE_DUE_DATE) + typeSpinner.setSelection(if (viewModel.repeatAfterCompletion!!) TYPE_COMPLETION_DATE else TYPE_DUE_DATE) refreshDisplayView() } @OnItemSelected(R.id.repeatType) fun onRepeatTypeChanged(position: Int) { - repeatAfterCompletion = position == TYPE_COMPLETION_DATE - repeatTypes[0] = if (repeatAfterCompletion) repeatTypes[2] else repeatTypes[1] + viewModel.repeatAfterCompletion = position == TYPE_COMPLETION_DATE + repeatTypes[0] = if (viewModel.repeatAfterCompletion!!) repeatTypes[2] else repeatTypes[1] typeAdapter.notifyDataSetChanged() } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString(EXTRA_RECURRENCE, if (rrule == null) "" else rrule!!.toIcal()) - outState.putBoolean(EXTRA_REPEAT_AFTER_COMPLETION, repeatAfterCompletion) - outState.putLong(EXTRA_DUE_DATE, dueDate) - } + private val dueDate: Long + get() = viewModel.dueDate!!.let { if (it > 0) it else currentTimeMillis() } override fun onRowClick() { - BasicRecurrenceDialog.newBasicRecurrenceDialog(this, rrule, dueDate) + BasicRecurrenceDialog.newBasicRecurrenceDialog(this, viewModel.rrule, dueDate) .show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE) } - override val isClickable: Boolean - get() = true + override val isClickable = true - override val layout: Int - get() = R.layout.control_set_repeat_display + override val layout = R.layout.control_set_repeat_display - override val icon: Int - get() = R.drawable.ic_outline_repeat_24px + override val icon = R.drawable.ic_outline_repeat_24px override fun controlId() = TAG - override suspend fun hasChanges(original: Task): Boolean { - val repeatUntil = rrule?.let { DateTime.from(it.until).millis } ?: 0 - return recurrenceValue != original.recurrence.orEmpty() - || original.repeatUntil != repeatUntil - } - - override suspend fun apply(task: Task) { - task.repeatUntil = if (rrule == null) 0 else DateTime.from(rrule!!.until).millis - task.recurrence = recurrenceValue - } - - private val recurrenceValue: String - get() { - if (rrule == null) { - return "" - } - val copy: RRule = try { - RRule(rrule!!.toIcal()) - } catch (e: ParseException) { - return "" - } - copy.until = null - var result = copy.toIcal() - if (repeatAfterCompletion && !isNullOrEmpty(result)) { - result += ";FROM=COMPLETION" // $NON-NLS-1$ - } - return result - } - private fun refreshDisplayView() { - if (rrule == null) { - displayView.text = null - repeatTypeContainer.visibility = View.GONE - } else { - displayView.text = repeatRuleToString.toString(rrule) - repeatTypeContainer.visibility = View.VISIBLE + viewModel.rrule.let { + if (it == null) { + displayView.text = null + repeatTypeContainer.visibility = View.GONE + } else { + displayView.text = repeatRuleToString.toString(it) + repeatTypeContainer.visibility = View.VISIBLE + } } } @@ -214,8 +145,5 @@ class RepeatControlSet : TaskEditControlFragment() { private const val TYPE_DUE_DATE = 1 private const val TYPE_COMPLETION_DATE = 2 private const val FRAG_TAG_BASIC_RECURRENCE = "frag_tag_basic_recurrence" - private const val EXTRA_RECURRENCE = "extra_recurrence" - private const val EXTRA_DUE_DATE = "extra_due_date" - private const val EXTRA_REPEAT_AFTER_COMPLETION = "extra_repeat_after_completion" } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskCreator.java b/app/src/main/java/com/todoroo/astrid/service/TaskCreator.java index e28c52ebe..80015e9b4 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskCreator.java +++ b/app/src/main/java/com/todoroo/astrid/service/TaskCreator.java @@ -4,7 +4,6 @@ import static com.todoroo.andlib.utility.DateUtilities.now; import static com.todoroo.astrid.helper.UUIDHelper.newUUID; import static org.tasks.Strings.isNullOrEmpty; -import android.content.ContentValues; import android.net.Uri; import androidx.annotation.Nullable; import com.todoroo.andlib.utility.DateUtilities; @@ -87,7 +86,7 @@ public class TaskCreator { if (!isNullOrEmpty(task.getTitle()) && gcalCreateEventEnabled && isNullOrEmpty(task.getCalendarURI())) { - Uri calendarUri = gcalHelper.createTaskEvent(task, new ContentValues()); + Uri calendarUri = gcalHelper.createTaskEvent(task, preferences.getDefaultCalendar()); task.setCalendarURI(calendarUri.toString()); } diff --git a/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.kt b/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.kt index 23eb2f88e..f19b966c8 100644 --- a/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.kt @@ -12,18 +12,12 @@ import android.view.View import android.widget.TextView import butterknife.BindView import com.google.android.material.chip.ChipGroup -import com.todoroo.andlib.utility.DateUtilities -import com.todoroo.astrid.activity.TaskEditFragment -import com.todoroo.astrid.data.Task import dagger.hilt.android.AndroidEntryPoint import org.tasks.R -import org.tasks.data.TagDao import org.tasks.data.TagData -import org.tasks.data.TagDataDao import org.tasks.tags.TagPickerActivity import org.tasks.ui.ChipProvider import org.tasks.ui.TaskEditControlFragment -import java.util.* import javax.inject.Inject /** @@ -33,8 +27,6 @@ import javax.inject.Inject */ @AndroidEntryPoint class TagsControlSet : TaskEditControlFragment() { - @Inject lateinit var tagDao: TagDao - @Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var chipProvider: ChipProvider @BindView(R.id.no_tags) @@ -43,71 +35,43 @@ class TagsControlSet : TaskEditControlFragment() { @BindView(R.id.chip_group) lateinit var chipGroup: ChipGroup - private lateinit var originalTags: ArrayList - private lateinit var selectedTags: ArrayList - override fun createView(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - originalTags = requireArguments().getParcelableArrayList(TaskEditFragment.EXTRA_TAGS)!! - selectedTags = ArrayList(originalTags) - refreshDisplayView() - } else { - selectedTags = savedInstanceState.getParcelableArrayList(EXTRA_SELECTED_TAGS)!! - originalTags = savedInstanceState.getParcelableArrayList(EXTRA_ORIGINAL_TAGS)!! - refreshDisplayView() - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelableArrayList(EXTRA_SELECTED_TAGS, selectedTags) - outState.putParcelableArrayList(EXTRA_ORIGINAL_TAGS, originalTags) - } - - override val layout: Int - get() = R.layout.control_set_tags - - override suspend fun apply(task: Task) { - if (tagDao.applyTags(task, tagDataDao, selectedTags)) { - task.modificationDate = DateUtilities.now() - } + refreshDisplayView() } override fun onRowClick() { val intent = Intent(context, TagPickerActivity::class.java) - intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, selectedTags) + intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, viewModel.selectedTags) startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY) } - override val isClickable: Boolean - get() = true + override val layout = R.layout.control_set_tags - override val icon: Int - get() = R.drawable.ic_outline_label_24px + override val isClickable = true - override fun controlId() = TAG + override val icon = R.drawable.ic_outline_label_24px - override suspend fun hasChanges(original: Task): Boolean { - return HashSet(originalTags) != HashSet(selectedTags) - } + override fun controlId() = TAG private fun refreshDisplayView() { - if (selectedTags.isEmpty()) { - chipGroup.visibility = View.GONE - tagsDisplay.visibility = View.VISIBLE - } else { - tagsDisplay.visibility = View.GONE - chipGroup.visibility = View.VISIBLE - chipGroup.removeAllViews() - for (tagData in selectedTags.sortedBy(TagData::name)) { - val chip = chipProvider.newClosableChip(tagData) - chipProvider.apply(chip, tagData) - chip.setOnClickListener { onRowClick() } - chip.setOnCloseIconClickListener { - selectedTags.remove(tagData) - refreshDisplayView() + viewModel.selectedTags?.let { selectedTags -> + if (selectedTags.isEmpty()) { + chipGroup.visibility = View.GONE + tagsDisplay.visibility = View.VISIBLE + } else { + tagsDisplay.visibility = View.GONE + chipGroup.visibility = View.VISIBLE + chipGroup.removeAllViews() + for (tagData in selectedTags.sortedBy(TagData::name)) { + val chip = chipProvider.newClosableChip(tagData) + chipProvider.apply(chip, tagData) + chip.setOnClickListener { onRowClick() } + chip.setOnCloseIconClickListener { + selectedTags.remove(tagData) + refreshDisplayView() + } + chipGroup.addView(chip) } - chipGroup.addView(chip) } } } @@ -115,7 +79,8 @@ class TagsControlSet : TaskEditControlFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) { if (resultCode == Activity.RESULT_OK && data != null) { - selectedTags = data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!! + viewModel.selectedTags = + data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED) refreshDisplayView() } } else { @@ -123,12 +88,8 @@ class TagsControlSet : TaskEditControlFragment() { } } - override fun requiresId() = true - companion object { const val TAG = R.string.TEA_ctrl_lists_pref - private const val EXTRA_ORIGINAL_TAGS = "extra_original_tags" - private const val EXTRA_SELECTED_TAGS = "extra_selected_tags" private const val REQUEST_TAG_PICKER_ACTIVITY = 10582 } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/timers/TimerControlSet.kt b/app/src/main/java/com/todoroo/astrid/timers/TimerControlSet.kt index 2ae48d312..5a32c973b 100644 --- a/app/src/main/java/com/todoroo/astrid/timers/TimerControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/timers/TimerControlSet.kt @@ -51,28 +51,16 @@ class TimerControlSet : TaskEditControlFragment() { private lateinit var estimated: TimeDurationControlSet private lateinit var elapsed: TimeDurationControlSet - private var timerStarted: Long = 0 private var dialog: AlertDialog? = null private lateinit var dialogView: View private lateinit var callback: TimerControlSetCallback override fun createView(savedInstanceState: Bundle?) { - val elapsedSeconds: Int - val estimatedSeconds: Int - if (savedInstanceState == null) { - timerStarted = task.timerStart - elapsedSeconds = task.elapsedSeconds - estimatedSeconds = task.estimatedSeconds - } else { - timerStarted = savedInstanceState.getLong(EXTRA_STARTED) - elapsedSeconds = savedInstanceState.getInt(EXTRA_ELAPSED) - estimatedSeconds = savedInstanceState.getInt(EXTRA_ESTIMATED) - } dialogView = activity.layoutInflater.inflate(R.layout.control_set_timers_dialog, null) estimated = TimeDurationControlSet(activity, dialogView, R.id.estimatedDuration, theme) elapsed = TimeDurationControlSet(activity, dialogView, R.id.elapsedDuration, theme) - estimated.setTimeDuration(estimatedSeconds) - elapsed.setTimeDuration(elapsedSeconds) + estimated.setTimeDuration(viewModel.estimatedSeconds!!) + elapsed.setTimeDuration(viewModel.elapsedSeconds!!) refresh() } @@ -81,13 +69,6 @@ class TimerControlSet : TaskEditControlFragment() { callback = activity as TimerControlSetCallback } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putInt(EXTRA_ELAPSED, elapsed.timeDurationInSeconds) - outState.putInt(EXTRA_ESTIMATED, estimated.timeDurationInSeconds) - outState.putLong(EXTRA_STARTED, timerStarted) - } - override fun onRowClick() { if (dialog == null) { dialog = buildDialog() @@ -95,9 +76,6 @@ class TimerControlSet : TaskEditControlFragment() { dialog!!.show() } - override val isClickable: Boolean - get() = true - private fun buildDialog(): AlertDialog { return dialogBuilder .newDialog() @@ -112,34 +90,24 @@ class TimerControlSet : TaskEditControlFragment() { if (timerActive()) { val task = callback.stopTimer() elapsed.setTimeDuration(task.elapsedSeconds) - timerStarted = 0 + viewModel.timerStarted = 0 chronometer.stop() refreshDisplayView() } else { val task = callback.startTimer() - timerStarted = task.timerStart + viewModel.timerStarted = task.timerStart chronometer.start() } updateChronometer() } - override val layout: Int - get() = R.layout.control_set_timers + override val layout = R.layout.control_set_timers - override val icon: Int - get() = R.drawable.ic_outline_timer_24px + override val icon = R.drawable.ic_outline_timer_24px override fun controlId() = TAG - override suspend fun hasChanges(original: Task): Boolean { - return (elapsed.timeDurationInSeconds != original.elapsedSeconds - || estimated.timeDurationInSeconds != original.estimatedSeconds) - } - - override suspend fun apply(task: Task) { - task.elapsedSeconds = elapsed.timeDurationInSeconds - task.estimatedSeconds = estimated.timeDurationInSeconds - } + override val isClickable = true private fun refresh() { refreshDisplayView() @@ -148,14 +116,18 @@ class TimerControlSet : TaskEditControlFragment() { private fun refreshDisplayView() { var est: String? = null - val estimatedSeconds = estimated.timeDurationInSeconds - if (estimatedSeconds > 0) { - est = getString(R.string.TEA_timer_est, DateUtils.formatElapsedTime(estimatedSeconds.toLong())) + viewModel.estimatedSeconds = estimated.timeDurationInSeconds + if (viewModel.estimatedSeconds!! > 0) { + est = getString( + R.string.TEA_timer_est, + DateUtils.formatElapsedTime(viewModel.estimatedSeconds!!.toLong())) } var elap: String? = null - val elapsedSeconds = elapsed.timeDurationInSeconds - if (elapsedSeconds > 0) { - elap = getString(R.string.TEA_timer_elap, DateUtils.formatElapsedTime(elapsedSeconds.toLong())) + viewModel.elapsedSeconds = elapsed.timeDurationInSeconds + if (viewModel.elapsedSeconds!! > 0) { + elap = getString( + R.string.TEA_timer_elap, + DateUtils.formatElapsedTime(viewModel.elapsedSeconds!!.toLong())) } val toDisplay: String? toDisplay = if (!isNullOrEmpty(est) && !isNullOrEmpty(elap)) { @@ -176,7 +148,7 @@ class TimerControlSet : TaskEditControlFragment() { var elapsed = elapsed.timeDurationInSeconds * 1000L if (timerActive()) { chronometer.visibility = View.VISIBLE - elapsed += DateUtilities.now() - timerStarted + elapsed += DateUtilities.now() - viewModel.timerStarted chronometer.base = SystemClock.elapsedRealtime() - elapsed if (elapsed > DateUtilities.ONE_DAY) { chronometer.onChronometerTickListener = OnChronometerTickListener { cArg: Chronometer -> @@ -191,9 +163,7 @@ class TimerControlSet : TaskEditControlFragment() { } } - private fun timerActive(): Boolean { - return timerStarted > 0 - } + private fun timerActive() = viewModel.timerStarted > 0 interface TimerControlSetCallback { fun stopTimer(): Task @@ -202,8 +172,5 @@ class TimerControlSet : TaskEditControlFragment() { companion object { const val TAG = R.string.TEA_ctrl_timer_pref - private const val EXTRA_STARTED = "extra_started" - private const val EXTRA_ESTIMATED = "extra_estimated" - private const val EXTRA_ELAPSED = "extra_elapsed" } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/ui/HideUntilControlSet.kt b/app/src/main/java/com/todoroo/astrid/ui/HideUntilControlSet.kt index 3cd496a73..b4de8d7f8 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/HideUntilControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/HideUntilControlSet.kt @@ -67,9 +67,6 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener { spinner.performClick() } - override val isClickable: Boolean - get() = true - override fun createView(savedInstanceState: Bundle?) { adapter = object : HiddenTopArrayAdapter( activity, android.R.layout.simple_spinner_item, spinnerItems) { @@ -94,8 +91,8 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener { } } if (savedInstanceState == null) { - val dueDate = task.dueDate - var hideUntil = task.hideUntil + val dueDate = viewModel.dueDate!! + var hideUntil = viewModel.hideUntil!! val dueDay = DateTimeUtils.newDateTime(dueDate) .withHourOfDay(0) .withMinuteOfHour(0) @@ -107,7 +104,7 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener { if (hideUntil <= 0) { selection = 0 hideUntil = 0 - if (task.isNew) { + if (viewModel.isNew) { when (preferences.getIntegerFromString(R.string.p_default_hideUntil_key, Task.HIDE_UNTIL_NONE)) { Task.HIDE_UNTIL_DUE -> selection = 1 Task.HIDE_UNTIL_DAY_BEFORE -> selection = 3 @@ -138,15 +135,13 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener { refreshDisplayView() } - override val layout: Int - get() = R.layout.control_set_hide + override val layout = R.layout.control_set_hide - override val icon: Int - get() = R.drawable.ic_outline_visibility_off_24px + override val icon = R.drawable.ic_outline_visibility_off_24px - override fun controlId(): Int { - return TAG - } + override fun controlId() = TAG + + override val isClickable = true override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_HIDE_UNTIL) { @@ -158,18 +153,6 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener { } } - override suspend fun apply(task: Task) { - task.hideUntil = getHideUntil(task) - } - - override suspend fun hasChanges(original: Task): Boolean { - return original.hideUntil != getHideUntil(original) - } - - private fun getHideUntil(task: Task): Long { - return task.createHideUntil(selectedValue!!.setting, selectedValue!!.date) - } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(EXTRA_CUSTOM, existingDate) @@ -251,6 +234,8 @@ class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener { private fun refreshDisplayView() { selectedValue = adapter.getItem(selection) + viewModel.hideUntil = viewModel.task + ?.createHideUntil(selectedValue!!.setting, selectedValue!!.date) clearButton.visibility = if (selectedValue!!.setting == Task.HIDE_UNTIL_NONE) View.GONE else View.VISIBLE } diff --git a/app/src/main/java/com/todoroo/astrid/ui/RandomReminderControlSet.kt b/app/src/main/java/com/todoroo/astrid/ui/RandomReminderControlSet.kt index 437c69dc9..8110691ce 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/RandomReminderControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/RandomReminderControlSet.kt @@ -13,24 +13,23 @@ import android.widget.ArrayAdapter import android.widget.Spinner import com.todoroo.andlib.utility.DateUtilities import org.tasks.R +import org.tasks.ui.TaskEditViewModel /** * Control set dealing with random reminder settings * * @author Tim Su @todoroo.com> */ -internal class RandomReminderControlSet(context: Context, parentView: View, reminderPeriod: Long) { - private val hours: IntArray - private var selectedIndex = 0 - val reminderPeriod: Long - get() { - val hourValue = hours[selectedIndex] - return hourValue * DateUtilities.ONE_HOUR - } - +internal class RandomReminderControlSet(context: Context, parentView: View, reminderPeriod: Long, vm: TaskEditViewModel) { init { val periodSpinner = parentView.findViewById(R.id.reminder_random_interval) periodSpinner.visibility = View.VISIBLE + // create hour array + val hourStrings = context.resources.getStringArray(R.array.TEA_reminder_random_hours) + val hours = IntArray(hourStrings.size) + for (i in hours.indices) { + hours[i] = hourStrings[i].toInt() + } // create adapter val adapter = ArrayAdapter( context, @@ -39,19 +38,13 @@ internal class RandomReminderControlSet(context: Context, parentView: View, remi adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) periodSpinner.adapter = adapter periodSpinner.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) { - selectedIndex = position + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + vm.reminderPeriod = hours[position] * DateUtilities.ONE_HOUR } override fun onNothingSelected(parent: AdapterView<*>?) {} } - // create hour array - val hourStrings = context.resources.getStringArray(R.array.TEA_reminder_random_hours) - hours = IntArray(hourStrings.size) - for (i in hours.indices) { - hours[i] = hourStrings[i].toInt() - } var i = 0 while (i < hours.size - 1) { if (hours[i] * DateUtilities.ONE_HOUR >= reminderPeriod) { diff --git a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt index 760306dbe..0bb7d7e9f 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt @@ -14,17 +14,13 @@ import android.view.View import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.StringRes -import androidx.lifecycle.lifecycleScope import butterknife.BindView import butterknife.OnClick import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.alarms.AlarmService -import com.todoroo.astrid.data.Task import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch import org.tasks.R import org.tasks.activities.DateAndTimePickerActivity -import org.tasks.data.Alarm import org.tasks.date.DateTimeUtils import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.MyTimePickerDialog @@ -41,8 +37,6 @@ import javax.inject.Inject */ @AndroidEntryPoint class ReminderControlSet : TaskEditControlFragment() { - private val alarms: MutableSet = LinkedHashSet() - @Inject lateinit var activity: Activity @Inject lateinit var alarmService: AlarmService @Inject lateinit var locale: Locale @@ -54,38 +48,35 @@ class ReminderControlSet : TaskEditControlFragment() { @BindView(R.id.reminder_alarm) lateinit var mode: TextView - private var taskId: Long = 0 - private var flags = 0 - private var randomReminder: Long = 0 - private var ringMode = 0 private var randomControlSet: RandomReminderControlSet? = null - private var whenDue = false - private var whenOverdue = false override fun createView(savedInstanceState: Bundle?) { mode.paintFlags = mode.paintFlags or Paint.UNDERLINE_TEXT_FLAG - taskId = task.id - if (savedInstanceState == null) { - flags = task.reminderFlags - randomReminder = task.reminderPeriod - } else { - flags = savedInstanceState.getInt(EXTRA_FLAGS) - randomReminder = savedInstanceState.getLong(EXTRA_RANDOM_REMINDER) + when { + viewModel.ringNonstop!! -> setRingMode(2) + viewModel.ringFiveTimes!! -> setRingMode(1) + else -> setRingMode(0) } - setup(savedInstanceState) - } - - private suspend fun currentAlarms(): List { - return if (taskId == Task.NO_ID) { - emptyList() - } else { - alarmService.getAlarms(taskId).map(Alarm::time) + if (viewModel.whenDue!!) { + addDue() + } + if (viewModel.whenOverdue!!) { + addOverdue() + } + if (viewModel.reminderPeriod!! > 0) { + addRandomReminder(viewModel.reminderPeriod!!) } + viewModel.selectedAlarms?.forEach(this::addAlarmRow) } @OnClick(R.id.reminder_alarm) fun onClickRingType() { val modes = resources.getStringArray(R.array.reminder_ring_modes) + val ringMode = when { + viewModel.ringNonstop == true -> 2 + viewModel.ringFiveTimes == true -> 1 + else -> 0 + } dialogBuilder .newDialog() .setSingleChoiceItems(modes, ringMode) { dialog: DialogInterface, which: Int -> @@ -96,7 +87,8 @@ class ReminderControlSet : TaskEditControlFragment() { } private fun setRingMode(ringMode: Int) { - this.ringMode = ringMode + viewModel.ringNonstop = ringMode == 2 + viewModel.ringFiveTimes = ringMode == 1 mode.setText(getRingModeString(ringMode)) } @@ -126,9 +118,7 @@ class ReminderControlSet : TaskEditControlFragment() { } else { dialogBuilder .newDialog() - .setItems( - options - ) { dialog: DialogInterface, which: Int -> + .setItems(options) { dialog: DialogInterface, which: Int -> addAlarm(options[which]) dialog.dismiss() } @@ -136,62 +126,19 @@ class ReminderControlSet : TaskEditControlFragment() { } } - override val layout: Int - get() = R.layout.control_set_reminders - - override val icon: Int - get() = R.drawable.ic_outline_notifications_24px - - override fun controlId(): Int { - return TAG - } - - private fun setup(savedInstanceState: Bundle?) { - setValue(flags) - alertContainer.removeAllViews() - if (whenDue) { - addDue() - } - if (whenOverdue) { - addOverdue() - } - if (randomReminder > 0) { - addRandomReminder(randomReminder) - } - if (savedInstanceState == null) { - lifecycleScope.launch { - currentAlarms().forEach { addAlarmRow(it) } - } - } else { - savedInstanceState.getLongArray(EXTRA_ALARMS)?.forEach { addAlarmRow(it) } - } - } - - override suspend fun hasChanges(original: Task): Boolean { - return getFlags() != original.reminderFlags || randomReminderPeriod != original.reminderPeriod || HashSet(currentAlarms()) != alarms - } + override val layout = R.layout.control_set_reminders - override fun requiresId() = true + override val icon = R.drawable.ic_outline_notifications_24px - override suspend fun apply(task: Task) { - task.reminderFlags = getFlags() - task.reminderPeriod = randomReminderPeriod - if (alarmService.synchronizeAlarms(task.id, alarms)) { - task.modificationDate = DateUtilities.now() - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putInt(EXTRA_FLAGS, getFlags()) - outState.putLong(EXTRA_RANDOM_REMINDER, randomReminderPeriod) - outState.putLongArray(EXTRA_ALARMS, alarms.toLongArray()) - } + override fun controlId() = TAG override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_NEW_ALARM) { if (resultCode == Activity.RESULT_OK) { - addAlarmRow(data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L)) + val timestamp = data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L) + if (viewModel.selectedAlarms?.add(timestamp) == true) { + addAlarmRow(timestamp) + } } } else { super.onActivityResult(requestCode, resultCode, data) @@ -199,31 +146,11 @@ class ReminderControlSet : TaskEditControlFragment() { } private fun addAlarmRow(timestamp: Long) { - if (alarms.add(timestamp)) { - addAlarmRow(DateUtilities.getLongDateStringWithTime(timestamp, locale.locale), View.OnClickListener { alarms.remove(timestamp) }) - } + addAlarmRow( + DateUtilities.getLongDateStringWithTime(timestamp, locale.locale), + View.OnClickListener { viewModel.selectedAlarms?.remove(timestamp) }) } - private fun getFlags(): Int { - var value = 0 - if (whenDue) { - value = value or Task.NOTIFY_AT_DEADLINE - } - if (whenOverdue) { - value = value or Task.NOTIFY_AFTER_DEADLINE - } - value = value and (Task.NOTIFY_MODE_FIVE or Task.NOTIFY_MODE_NONSTOP).inv() - if (ringMode == 2) { - value = value or Task.NOTIFY_MODE_NONSTOP - } else if (ringMode == 1) { - value = value or Task.NOTIFY_MODE_FIVE - } - return value - } - - private val randomReminderPeriod: Long - get() = if (randomControlSet == null) 0L else randomControlSet!!.reminderPeriod - private fun addNewAlarm() { val intent = Intent(activity, DateAndTimePickerActivity::class.java) intent.putExtra( @@ -252,10 +179,10 @@ class ReminderControlSet : TaskEditControlFragment() { private val options: List get() { val options: MutableList = ArrayList() - if (!whenDue) { + if (viewModel.whenDue != true) { options.add(getString(R.string.when_due)) } - if (!whenOverdue) { + if (viewModel.whenOverdue != true) { options.add(getString(R.string.when_overdue)) } if (randomControlSet == null) { @@ -266,35 +193,29 @@ class ReminderControlSet : TaskEditControlFragment() { } private fun addDue() { - whenDue = true - addAlarmRow(getString(R.string.when_due), View.OnClickListener { whenDue = false }) + viewModel.whenDue = true + addAlarmRow(getString(R.string.when_due), View.OnClickListener { + viewModel.whenDue = false + }) } private fun addOverdue() { - whenOverdue = true - addAlarmRow(getString(R.string.when_overdue), View.OnClickListener { whenOverdue = false }) + viewModel.whenOverdue = true + addAlarmRow(getString(R.string.when_overdue), View.OnClickListener { + viewModel.whenOverdue = false + }) } private fun addRandomReminder(reminderPeriod: Long) { - val alarmRow = addAlarmRow(getString(R.string.randomly_once) + " ", View.OnClickListener { randomControlSet = null }) - randomControlSet = RandomReminderControlSet(activity, alarmRow, reminderPeriod) - } - - private fun setValue(flags: Int) { - whenDue = flags and Task.NOTIFY_AT_DEADLINE > 0 - whenOverdue = flags and Task.NOTIFY_AFTER_DEADLINE > 0 - when { - flags and Task.NOTIFY_MODE_NONSTOP > 0 -> setRingMode(2) - flags and Task.NOTIFY_MODE_FIVE > 0 -> setRingMode(1) - else -> setRingMode(0) - } + val alarmRow = addAlarmRow(getString(R.string.randomly_once) + " ", View.OnClickListener { + viewModel.reminderPeriod = 0 + randomControlSet = null + }) + randomControlSet = RandomReminderControlSet(activity, alarmRow, reminderPeriod, viewModel) } companion object { const val TAG = R.string.TEA_ctrl_reminders_pref private const val REQUEST_NEW_ALARM = 12152 - private const val EXTRA_FLAGS = "extra_flags" - private const val EXTRA_RANDOM_REMINDER = "extra_random_reminder" - private const val EXTRA_ALARMS = "extra_alarms" } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/Event.java b/app/src/main/java/org/tasks/Event.java index 1934925bc..db4437a7d 100644 --- a/app/src/main/java/org/tasks/Event.java +++ b/app/src/main/java/org/tasks/Event.java @@ -16,4 +16,8 @@ public class Event { handled = true; return value; } + + public T getValue() { + return value; + } } diff --git a/app/src/main/java/org/tasks/data/AlarmDao.kt b/app/src/main/java/org/tasks/data/AlarmDao.kt index 2c3c5e9d9..7efe410ac 100644 --- a/app/src/main/java/org/tasks/data/AlarmDao.kt +++ b/app/src/main/java/org/tasks/data/AlarmDao.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query +import com.todoroo.astrid.data.Task @Dao interface AlarmDao { @@ -28,4 +29,10 @@ interface AlarmDao { @Insert suspend fun insert(alarms: Iterable) + + suspend fun getAlarms(task: Task) = ArrayList(if (task.isNew) { + emptyList() + } else { + getAlarms(task.id) + }) } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/fragments/CommentBarFragment.kt b/app/src/main/java/org/tasks/fragments/CommentBarFragment.kt index 35d2bad65..9b5bc50a7 100644 --- a/app/src/main/java/org/tasks/fragments/CommentBarFragment.kt +++ b/app/src/main/java/org/tasks/fragments/CommentBarFragment.kt @@ -83,16 +83,12 @@ class CommentBarFragment : TaskEditControlFragment() { resetPictureButton() } - override val layout: Int - get() = R.layout.fragment_comment_bar + override val layout = R.layout.fragment_comment_bar - override val icon: Int - get() = 0 + override val icon = 0 override fun controlId() = TAG - override suspend fun apply(task: Task) {} - @OnTextChanged(R.id.commentField) fun onTextChanged(s: CharSequence) { commentButton.visibility = if (pendingCommentPicture == null && isNullOrEmpty(s.toString())) View.GONE else View.VISIBLE diff --git a/app/src/main/java/org/tasks/fragments/TaskEditControlSetFragmentManager.kt b/app/src/main/java/org/tasks/fragments/TaskEditControlSetFragmentManager.kt index d59728664..c23cae9e5 100644 --- a/app/src/main/java/org/tasks/fragments/TaskEditControlSetFragmentManager.kt +++ b/app/src/main/java/org/tasks/fragments/TaskEditControlSetFragmentManager.kt @@ -1,10 +1,8 @@ package org.tasks.fragments import android.content.Context -import android.os.Bundle import androidx.fragment.app.FragmentManager import com.todoroo.astrid.activity.BeastModePreferences -import com.todoroo.astrid.data.Task import com.todoroo.astrid.files.FilesControlSet import com.todoroo.astrid.repeats.RepeatControlSet import com.todoroo.astrid.tags.TagsControlSet @@ -27,17 +25,7 @@ class TaskEditControlSetFragmentManager @Inject constructor( private val displayOrder: List var visibleSize = 0 - fun getFragmentsInPersistOrder(fragmentManager: FragmentManager): List { - return controlSetFragments.keys - .mapNotNull { fragmentManager.findFragmentByTag(it) as TaskEditControlFragment? } - } - - fun getOrCreateFragments( - fragmentManager: FragmentManager, - task: Task, - arguments: Bundle): List { - arguments.putParcelable(TaskEditControlFragment.EXTRA_TASK, task) - arguments.putBoolean(TaskEditControlFragment.EXTRA_IS_NEW, task.isNew) + fun getOrCreateFragments(fragmentManager: FragmentManager): List { val fragments: MutableList = ArrayList() for (i in displayOrder.indices) { val tag = displayOrder[i] @@ -45,7 +33,6 @@ class TaskEditControlSetFragmentManager @Inject constructor( if (fragment == null) { val resId = controlSetFragments[tag] fragment = createFragment(resId!!) - fragment.arguments = arguments } fragments.add(fragment) } diff --git a/app/src/main/java/org/tasks/ui/CalendarControlSet.kt b/app/src/main/java/org/tasks/ui/CalendarControlSet.kt index 529a955c0..9c2fddfaf 100644 --- a/app/src/main/java/org/tasks/ui/CalendarControlSet.kt +++ b/app/src/main/java/org/tasks/ui/CalendarControlSet.kt @@ -1,19 +1,14 @@ package org.tasks.ui import android.app.Activity -import android.content.ContentValues import android.content.Intent import android.net.Uri -import android.os.Bundle import android.provider.CalendarContract -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import butterknife.BindView import butterknife.OnClick -import com.todoroo.astrid.data.Task import com.todoroo.astrid.gcal.GCalHelper import dagger.hilt.android.AndroidEntryPoint import org.tasks.PermissionUtil.verifyPermissions @@ -50,108 +45,33 @@ class CalendarControlSet : TaskEditControlFragment() { @Inject lateinit var themeBase: ThemeBase @Inject lateinit var calendarEventProvider: CalendarEventProvider - private var calendarId: String? = null - private var eventUri: String? = null + override fun onResume() { + super.onResume() - override fun createView(savedInstanceState: Bundle?) { val canAccessCalendars = permissionChecker.canAccessCalendars() - if (savedInstanceState != null) { - eventUri = savedInstanceState.getString(EXTRA_URI) - calendarId = savedInstanceState.getString(EXTRA_ID) - } else if (task.isNew && canAccessCalendars) { - calendarId = preferences.defaultCalendar - if (!isNullOrEmpty(calendarId)) { - try { - val defaultCalendar = calendarProvider.getCalendar(calendarId) - if (defaultCalendar == null) { - calendarId = null - } - } catch (e: Exception) { - Timber.e(e) - firebase.reportException(e) - calendarId = null - } + viewModel.eventUri?.let { + if (canAccessCalendars && !calendarEntryExists(it)) { + viewModel.eventUri = null } - } else { - eventUri = task.calendarURI } - if (canAccessCalendars && !calendarEntryExists(eventUri)) { - eventUri = null + if (!canAccessCalendars) { + viewModel.selectedCalendar = null } + refreshDisplayView() } - override val layout: Int - get() = R.layout.control_set_gcal_display + override val layout = R.layout.control_set_gcal_display - override val icon: Int - get() = R.drawable.ic_outline_event_24px + override val icon = R.drawable.ic_outline_event_24px override fun controlId() = TAG - override suspend fun hasChanges(original: Task): Boolean { - if (!permissionChecker.canAccessCalendars()) { - return false - } - if (!isNullOrEmpty(calendarId)) { - return true - } - val originalUri = original.calendarURI - return if (isNullOrEmpty(eventUri) && isNullOrEmpty(originalUri)) { - false - } else originalUri != eventUri - } - - override suspend fun apply(task: Task) { - if (!permissionChecker.canAccessCalendars()) { - return - } - if (!isNullOrEmpty(task.calendarURI)) { - if (eventUri == null) { - calendarEventProvider.deleteEvent(task) - } else if (!calendarEntryExists(task.calendarURI)) { - task.calendarURI = "" - } - } - if (!task.hasDueDate()) { - return - } - if (calendarEntryExists(task.calendarURI)) { - val cr = activity.contentResolver - try { - val updateValues = ContentValues() - - // check if we need to update the item - updateValues.put(CalendarContract.Events.TITLE, task.title) - updateValues.put(CalendarContract.Events.DESCRIPTION, task.notes) - gcalHelper.createStartAndEndDate(task, updateValues) - cr.update(Uri.parse(task.calendarURI), updateValues, null, null) - } catch (e: Exception) { - Timber.e(e, "unable-to-update-calendar: %s", task.calendarURI) - } - } else if (!isNullOrEmpty(calendarId)) { - try { - val values = ContentValues() - values.put(CalendarContract.Events.CALENDAR_ID, calendarId) - val uri = gcalHelper.createTaskEvent(task, values) - if (uri != null) { - task.calendarURI = uri.toString() - } - } catch (e: Exception) { - Timber.e(e) - } - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString(EXTRA_URI, eventUri) - outState.putString(EXTRA_ID, calendarId) - } + override val isClickable = true @OnClick(R.id.clear) fun clearCalendar() { - if (isNullOrEmpty(eventUri)) { + if (viewModel.eventUri.isNullOrBlank()) { clear() } else { dialogBuilder @@ -167,13 +87,13 @@ class CalendarControlSet : TaskEditControlFragment() { } private fun clear() { - calendarId = null - eventUri = null + viewModel.selectedCalendar = null + viewModel.eventUri = null refreshDisplayView() } override fun onRowClick() { - if (isNullOrEmpty(eventUri)) { + if (viewModel.eventUri.isNullOrBlank()) { CalendarPicker.newCalendarPicker(this, REQUEST_CODE_PICK_CALENDAR, calendarName) .show(parentFragmentManager, FRAG_TAG_CALENDAR_PICKER) } else { @@ -183,12 +103,9 @@ class CalendarControlSet : TaskEditControlFragment() { } } - override val isClickable: Boolean - get() = true - private fun openCalendarEvent() { val cr = activity.contentResolver - val uri = Uri.parse(eventUri) + val uri = Uri.parse(viewModel.eventUri) val intent = Intent(Intent.ACTION_VIEW, uri) try { cr.query( @@ -199,7 +116,7 @@ class CalendarControlSet : TaskEditControlFragment() { if (cursor!!.count == 0) { // event no longer exists Toast.makeText(activity, R.string.calendar_event_not_found, Toast.LENGTH_SHORT).show() - eventUri = null + viewModel.eventUri = null refreshDisplayView() } else { cursor.moveToFirst() @@ -217,7 +134,7 @@ class CalendarControlSet : TaskEditControlFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_CODE_PICK_CALENDAR) { if (resultCode == Activity.RESULT_OK) { - calendarId = data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID) + viewModel.selectedCalendar = data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID) refreshDisplayView() } } else { @@ -226,13 +143,7 @@ class CalendarControlSet : TaskEditControlFragment() { } private val calendarName: String? - get() { - if (calendarId == null) { - return null - } - val calendar = calendarProvider.getCalendar(calendarId) - return calendar?.name - } + get() = viewModel.selectedCalendar?.let { calendarProvider.getCalendar(it)?.name } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray) { @@ -249,14 +160,16 @@ class CalendarControlSet : TaskEditControlFragment() { } } - private fun refreshDisplayView() { - if (!isNullOrEmpty(eventUri)) { + private fun refreshDisplayView() = when { + viewModel.eventUri?.isNotBlank() == true -> { calendar.setText(R.string.gcal_TEA_showCalendar_label) cancelButton.visibility = View.VISIBLE - } else if (calendarId != null) { + } + !viewModel.selectedCalendar.isNullOrBlank() -> { calendar.text = calendarName cancelButton.visibility = View.GONE - } else { + } + else -> { calendar.text = null cancelButton.visibility = View.GONE } @@ -287,7 +200,5 @@ class CalendarControlSet : TaskEditControlFragment() { private const val REQUEST_CODE_PICK_CALENDAR = 70 private const val REQUEST_CODE_OPEN_EVENT = 71 private const val REQUEST_CODE_CLEAR_EVENT = 72 - private const val EXTRA_URI = "extra_uri" - private const val EXTRA_ID = "extra_id" } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/DeadlineControlSet.kt b/app/src/main/java/org/tasks/ui/DeadlineControlSet.kt index cab9f1b84..6972dcd1b 100644 --- a/app/src/main/java/org/tasks/ui/DeadlineControlSet.kt +++ b/app/src/main/java/org/tasks/ui/DeadlineControlSet.kt @@ -6,8 +6,6 @@ import android.os.Bundle import android.widget.TextView import butterknife.BindView import com.todoroo.andlib.utility.DateUtilities -import com.todoroo.astrid.data.Task -import com.todoroo.astrid.data.Task.Companion.createDueDate import com.todoroo.astrid.data.Task.Companion.hasDueTime import dagger.hilt.android.AndroidEntryPoint import org.tasks.R @@ -16,7 +14,6 @@ import org.tasks.dialogs.DateTimePicker import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker import org.tasks.locale.Locale import org.tasks.preferences.Preferences -import org.tasks.time.DateTime import java.time.format.FormatStyle import javax.inject.Inject @@ -30,7 +27,6 @@ class DeadlineControlSet : TaskEditControlFragment() { lateinit var dueDate: TextView private lateinit var callback: DueDateChangeListener - private var date: Long = 0 override fun onAttach(activity: Activity) { super.onAttach(activity) @@ -38,7 +34,6 @@ class DeadlineControlSet : TaskEditControlFragment() { } override fun createView(savedInstanceState: Bundle?) { - date = savedInstanceState?.getLong(EXTRA_DATE) ?: task.dueDate refreshDisplayView() } @@ -48,42 +43,25 @@ class DeadlineControlSet : TaskEditControlFragment() { newDateTimePicker( this, REQUEST_DATE, - dueDateTime, + viewModel.dueDate!!, preferences.getBoolean(R.string.p_auto_dismiss_datetime_edit_screen, false)) .show(fragmentManager, FRAG_TAG_DATE_PICKER) } } - override val isClickable: Boolean - get() = true + override val isClickable = true - override val layout: Int - get() = R.layout.control_set_deadline + override val layout = R.layout.control_set_deadline - override val icon: Int - get() = R.drawable.ic_outline_schedule_24px + override val icon = R.drawable.ic_outline_schedule_24px override fun controlId() = TAG - override suspend fun hasChanges(original: Task): Boolean { - return original.dueDate != dueDateTime - } - - override suspend fun apply(task: Task) { - val dueDate = dueDateTime - if (dueDate != task.dueDate) { - task.reminderSnooze = 0L - } - task.dueDate = dueDate - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_DATE) { if (resultCode == Activity.RESULT_OK) { - val timestamp = data!!.getLongExtra(DateTimePicker.EXTRA_TIMESTAMP, 0L) - val dateTime = DateTime(timestamp) - date = dateTime.millis - callback.dueDateChanged(dueDateTime) + viewModel.dueDate = data!!.getLongExtra(DateTimePicker.EXTRA_TIMESTAMP, 0L) + callback.dueDateChanged() } refreshDisplayView() } else { @@ -91,24 +69,18 @@ class DeadlineControlSet : TaskEditControlFragment() { } } - private val dueDateTime: Long - get() = if (date == 0L) 0 else createDueDate( - if (hasDueTime(date)) Task.URGENCY_SPECIFIC_DAY_TIME else Task.URGENCY_SPECIFIC_DAY, - date) - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putLong(EXTRA_DATE, date) - } - private fun refreshDisplayView() { + val date = viewModel.dueDate!! if (date == 0L) { dueDate.text = "" setTextColor(false) } else { dueDate.text = DateUtilities.getRelativeDateTime(activity, date, locale.locale, FormatStyle.FULL) - setTextColor( - if (hasDueTime(date)) DateTimeUtils.newDateTime(date).isBeforeNow else DateTimeUtils.newDateTime(date).endOfDay().isBeforeNow) + setTextColor(if (hasDueTime(date)) { + DateTimeUtils.newDateTime(date).isBeforeNow + } else { + DateTimeUtils.newDateTime(date).endOfDay().isBeforeNow + }) } } @@ -118,13 +90,12 @@ class DeadlineControlSet : TaskEditControlFragment() { } interface DueDateChangeListener { - fun dueDateChanged(dateTime: Long) + fun dueDateChanged() } companion object { const val TAG = R.string.TEA_ctrl_when_pref private const val REQUEST_DATE = 504 - private const val EXTRA_DATE = "extra_date" private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker" } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/DescriptionControlSet.kt b/app/src/main/java/org/tasks/ui/DescriptionControlSet.kt index 621e25447..3b6350fac 100644 --- a/app/src/main/java/org/tasks/ui/DescriptionControlSet.kt +++ b/app/src/main/java/org/tasks/ui/DescriptionControlSet.kt @@ -4,10 +4,8 @@ import android.os.Bundle import android.widget.EditText import butterknife.BindView import butterknife.OnTextChanged -import com.todoroo.astrid.data.Task import dagger.hilt.android.AndroidEntryPoint import org.tasks.R -import org.tasks.Strings.isNullOrEmpty import org.tasks.dialogs.Linkify import org.tasks.preferences.Preferences import javax.inject.Inject @@ -20,53 +18,25 @@ class DescriptionControlSet : TaskEditControlFragment() { @BindView(R.id.notes) lateinit var editText: EditText - private var description: String? = null - override fun createView(savedInstanceState: Bundle?) { - description = if (savedInstanceState == null) { - stripCarriageReturns(task.notes) - } else { - savedInstanceState.getString(EXTRA_DESCRIPTION) - } - if (!isNullOrEmpty(description)) { - editText.setTextKeepState(description) - } + viewModel.description?.let(editText::setTextKeepState) if (preferences.getBoolean(R.string.p_linkify_task_edit, false)) { linkify.linkify(editText) } } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString(EXTRA_DESCRIPTION, description) - } - - override val layout: Int - get() = R.layout.control_set_description + override val layout = R.layout.control_set_description - override val icon: Int - get() = R.drawable.ic_outline_notes_24px + override val icon = R.drawable.ic_outline_notes_24px override fun controlId() = TAG @OnTextChanged(R.id.notes) fun textChanged(text: CharSequence) { - description = text.toString().trim { it <= ' ' } - } - - override suspend fun apply(task: Task) { - task.notes = description - } - - override suspend fun hasChanges(original: Task): Boolean { - return !if (isNullOrEmpty(description)) isNullOrEmpty(original.notes) else description == stripCarriageReturns(original.notes) + viewModel.description = text.toString().trim { it <= ' ' } } companion object { const val TAG = R.string.TEA_ctrl_notes_pref - private const val EXTRA_DESCRIPTION = "extra_description" - fun stripCarriageReturns(original: String?): String? { - return original?.replace("\\r\\n?".toRegex(), "\n") - } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/ListFragment.kt b/app/src/main/java/org/tasks/ui/ListFragment.kt index 174c11068..6467265f9 100644 --- a/app/src/main/java/org/tasks/ui/ListFragment.kt +++ b/app/src/main/java/org/tasks/ui/ListFragment.kt @@ -5,11 +5,9 @@ import android.content.Intent import android.os.Bundle import butterknife.BindView import com.google.android.material.chip.ChipGroup -import com.todoroo.astrid.activity.TaskEditFragment import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.GtasksFilter -import com.todoroo.astrid.data.Task import com.todoroo.astrid.service.TaskMover import dagger.hilt.android.AndroidEntryPoint import org.tasks.R @@ -21,11 +19,8 @@ class ListFragment : TaskEditControlFragment() { @BindView(R.id.chip_group) lateinit var chipGroup: ChipGroup - @Inject lateinit var taskMover: TaskMover @Inject lateinit var chipProvider: ChipProvider - private var originalList: Filter? = null - private lateinit var selectedList: Filter private lateinit var callback: OnListChanged interface OnListChanged { @@ -38,62 +33,38 @@ class ListFragment : TaskEditControlFragment() { } override fun createView(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - originalList = requireArguments().getParcelable(TaskEditFragment.EXTRA_LIST)!! - setSelected(originalList!!) - } else { - originalList = savedInstanceState.getParcelable(EXTRA_ORIGINAL_LIST) - setSelected(savedInstanceState.getParcelable(EXTRA_SELECTED_LIST)!!) - } + refreshView() } private fun setSelected(filter: Filter) { - selectedList = filter + viewModel.selectedList = filter refreshView() callback.onListChanged(filter) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelable(EXTRA_ORIGINAL_LIST, originalList) - outState.putParcelable(EXTRA_SELECTED_LIST, selectedList) - } - - override val layout: Int - get() = R.layout.control_set_remote_list + override val layout = R.layout.control_set_remote_list - override val icon: Int - get() = R.drawable.ic_list_24px + override val icon = R.drawable.ic_list_24px override fun controlId() = TAG override fun onRowClick() = openPicker() - override val isClickable: Boolean - get() = true + override val isClickable = true private fun openPicker() = - ListPicker.newListPicker(selectedList, this, REQUEST_CODE_SELECT_LIST) + ListPicker.newListPicker(viewModel.selectedList!!, this, REQUEST_CODE_SELECT_LIST) .show(parentFragmentManager, FRAG_TAG_GOOGLE_TASK_LIST_SELECTION) - override fun requiresId() = true - - override suspend fun apply(task: Task) { - if (isNew || hasChanges()) { - task.parent = 0 - taskMover.move(listOf(task.id), selectedList) - } - } - - override suspend fun hasChanges(original: Task) = hasChanges() - - private fun hasChanges() = selectedList != originalList - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_CODE_SELECT_LIST) { if (resultCode == Activity.RESULT_OK) { data?.getParcelableExtra(ListPicker.EXTRA_SELECTED_FILTER)?.let { - setList(it) + if (it is GtasksFilter || it is CaldavFilter) { + setSelected(it) + } else { + throw RuntimeException("Unhandled filter type") + } } } } else { @@ -101,17 +72,13 @@ class ListFragment : TaskEditControlFragment() { } } - private fun setList(list: Filter) { - if (list is GtasksFilter || list is CaldavFilter) { - setSelected(list) - } else { - throw RuntimeException("Unhandled filter type") - } - } - private fun refreshView() { chipGroup.removeAllViews() - val chip = chipProvider.newChip(selectedList, R.drawable.ic_list_24px, showText = true, showIcon = true)!! + val chip = chipProvider.newChip( + viewModel.selectedList!!, + R.drawable.ic_list_24px, + showText = true, + showIcon = true)!! chip.setOnClickListener { openPicker() } chipGroup.addView(chip) } @@ -119,8 +86,6 @@ class ListFragment : TaskEditControlFragment() { companion object { const val TAG = R.string.TEA_ctrl_google_task_list private const val FRAG_TAG_GOOGLE_TASK_LIST_SELECTION = "frag_tag_google_task_list_selection" - private const val EXTRA_ORIGINAL_LIST = "extra_original_list" - private const val EXTRA_SELECTED_LIST = "extra_selected_list" private const val REQUEST_CODE_SELECT_LIST = 10101 } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/LocationControlSet.kt b/app/src/main/java/org/tasks/ui/LocationControlSet.kt index 525f20d9f..b010043f3 100644 --- a/app/src/main/java/org/tasks/ui/LocationControlSet.kt +++ b/app/src/main/java/org/tasks/ui/LocationControlSet.kt @@ -14,21 +14,15 @@ import android.widget.TextView import androidx.core.util.Pair import butterknife.BindView import butterknife.OnClick -import com.todoroo.andlib.utility.DateUtilities -import com.todoroo.astrid.activity.TaskEditFragment -import com.todoroo.astrid.data.SyncFlags -import com.todoroo.astrid.data.Task import dagger.hilt.android.AndroidEntryPoint import org.tasks.PermissionUtil.verifyPermissions import org.tasks.R import org.tasks.Strings.isNullOrEmpty import org.tasks.data.Geofence import org.tasks.data.Location -import org.tasks.data.LocationDao import org.tasks.data.Place import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.GeofenceDialog -import org.tasks.location.GeofenceApi import org.tasks.location.LocationPickerActivity import org.tasks.preferences.* import java.util.* @@ -38,8 +32,6 @@ import javax.inject.Inject class LocationControlSet : TaskEditControlFragment() { @Inject lateinit var preferences: Preferences @Inject lateinit var dialogBuilder: DialogBuilder - @Inject lateinit var geofenceApi: GeofenceApi - @Inject lateinit var locationDao: LocationDao @Inject lateinit var device: Device @Inject lateinit var permissionRequestor: FragmentPermissionRequestor @Inject lateinit var permissionChecker: PermissionChecker @@ -52,33 +44,20 @@ class LocationControlSet : TaskEditControlFragment() { @BindView(R.id.geofence_options) lateinit var geofenceOptions: ImageView - - private var original: Location? = null - private var location: Location? = null - - override fun createView(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - original = requireArguments().getParcelable(TaskEditFragment.EXTRA_PLACE) - if (original != null) { - setLocation(Location(original!!.geofence, original!!.place)) - } - } else { - original = savedInstanceState.getParcelable(EXTRA_ORIGINAL) - location = savedInstanceState.getParcelable(EXTRA_LOCATION) - } - } override fun onResume() { super.onResume() + updateUi() } private fun setLocation(location: Location?) { - this.location = location + viewModel.selectedLocation = location updateUi() } private fun updateUi() { + val location = viewModel.selectedLocation if (location == null) { locationName.text = "" geofenceOptions.visibility = View.GONE @@ -87,9 +66,9 @@ class LocationControlSet : TaskEditControlFragment() { geofenceOptions.visibility = if (device.supportsGeofences()) View.VISIBLE else View.GONE geofenceOptions.setImageResource( if (permissionChecker.canAccessLocation() - && (location!!.isArrival || location!!.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px) - val name = location!!.displayName - val address = location!!.displayAddress + && (location.isArrival || location.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px) + val name = location.displayName + val address = location.displayAddress if (!isNullOrEmpty(address) && address != name) { locationAddress.text = address locationAddress.visibility = View.VISIBLE @@ -109,22 +88,23 @@ class LocationControlSet : TaskEditControlFragment() { } override fun onRowClick() { + val location = viewModel.selectedLocation if (location == null) { chooseLocation() } else { val options: MutableList Unit>> = ArrayList() - options.add(Pair.create(R.string.open_map, { location!!.open(activity) })) - if (!isNullOrEmpty(location!!.phone)) { + options.add(Pair.create(R.string.open_map, { location.open(activity) })) + if (!isNullOrEmpty(location.phone)) { options.add(Pair.create(R.string.action_call, { call() })) } - if (!isNullOrEmpty(location!!.url)) { + if (!isNullOrEmpty(location.url)) { options.add(Pair.create(R.string.visit_website, { openWebsite() })) } options.add(Pair.create(R.string.choose_new_location, { chooseLocation() })) options.add(Pair.create(R.string.delete, { setLocation(null) })) val items = options.map { requireContext().getString(it.first!!) } dialogBuilder - .newDialog(location!!.displayName) + .newDialog(location.displayName) .setItems(items) { _, which: Int -> options[which].second!!.invoke() } @@ -132,13 +112,10 @@ class LocationControlSet : TaskEditControlFragment() { } } - override val isClickable: Boolean - get() = true - private fun chooseLocation() { val intent = Intent(activity, LocationPickerActivity::class.java) - if (location != null) { - intent.putExtra(LocationPickerActivity.EXTRA_PLACE, location!!.place as Parcelable) + viewModel.selectedLocation?.let { + intent.putExtra(LocationPickerActivity.EXTRA_PLACE, it.place as Parcelable) } startActivityForResult(intent, REQUEST_LOCATION_REMINDER) } @@ -168,78 +145,40 @@ class LocationControlSet : TaskEditControlFragment() { } private fun showGeofenceOptions() { - val dialog = GeofenceDialog.newGeofenceDialog(location) + val dialog = GeofenceDialog.newGeofenceDialog(viewModel.selectedLocation) dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS) dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG) } - override val layout: Int - get() = R.layout.location_row + override val layout = R.layout.location_row - override val icon: Int - get() = R.drawable.ic_outline_place_24px + override val icon = R.drawable.ic_outline_place_24px override fun controlId() = TAG - private fun openWebsite() { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(location!!.url))) - } - - private fun call() { - startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + location!!.phone))) - } + override val isClickable = true - override suspend fun hasChanges(task: Task): Boolean { - if (original == null) { - return location != null - } - if (location == null) { - return true - } - return if (original!!.place != location!!.place) { - true - } else { - original!!.isDeparture != location!!.isDeparture - || original!!.isArrival != location!!.isArrival - || original!!.radius != location!!.radius + private fun openWebsite() { + viewModel.selectedLocation?.let { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url))) } } - override fun requiresId() = true - - override suspend fun apply(task: Task) { - if (original == null || location == null || original!!.place != location!!.place) { - task.putTransitory(SyncFlags.FORCE_CALDAV_SYNC, true) - } - if (original != null) { - locationDao.delete(original!!.geofence) - geofenceApi.update(original!!.place) - } - if (location != null) { - val place = location!!.place - val geofence = location!!.geofence - geofence.task = task.id - geofence.place = place.uid - geofence.id = locationDao.insert(geofence) - geofenceApi.update(place) + private fun call() { + viewModel.selectedLocation?.let { + startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.phone))) } - task.modificationDate = DateUtilities.now() - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelable(EXTRA_ORIGINAL, original) - outState.putParcelable(EXTRA_LOCATION, location) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_LOCATION_REMINDER) { if (resultCode == Activity.RESULT_OK) { val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!! + val location = viewModel.selectedLocation val geofence = if (location == null) { Geofence(place.uid, preferences) } else { - val existing = location!!.geofence + val existing = location.geofence Geofence( place.uid, existing.isArrival, @@ -250,7 +189,7 @@ class LocationControlSet : TaskEditControlFragment() { } } else if (requestCode == REQUEST_GEOFENCE_DETAILS) { if (resultCode == Activity.RESULT_OK) { - location!!.geofence = data!!.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE)!! + viewModel.selectedLocation?.geofence = data!!.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE)!! updateUi() } } else { @@ -263,7 +202,5 @@ class LocationControlSet : TaskEditControlFragment() { private const val REQUEST_LOCATION_REMINDER = 12153 private const val REQUEST_GEOFENCE_DETAILS = 12154 private const val FRAG_TAG_LOCATION_DIALOG = "location_dialog" - private const val EXTRA_ORIGINAL = "extra_original_location" - private const val EXTRA_LOCATION = "extra_new_location" } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/PriorityControlSet.kt b/app/src/main/java/org/tasks/ui/PriorityControlSet.kt index 4a8437d77..d591fd7d7 100644 --- a/app/src/main/java/org/tasks/ui/PriorityControlSet.kt +++ b/app/src/main/java/org/tasks/ui/PriorityControlSet.kt @@ -2,9 +2,6 @@ package org.tasks.ui import android.content.res.ColorStateList import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.appcompat.widget.AppCompatRadioButton import butterknife.BindView import butterknife.OnClick @@ -30,17 +27,13 @@ class PriorityControlSet : TaskEditControlFragment() { @BindView(R.id.priority_none) lateinit var priorityNone: AppCompatRadioButton - @Task.Priority - private var priority = 0 - @OnClick(R.id.priority_high, R.id.priority_medium, R.id.priority_low, R.id.priority_none) fun onPriorityChanged() { - priority = getPriority() + viewModel.priority = getPriority() } override fun createView(savedInstanceState: Bundle?) { - priority = savedInstanceState?.getInt(EXTRA_PRIORITY) ?: task.priority - when (priority) { + when (viewModel.priority) { 0 -> priorityHigh.isChecked = true 1 -> priorityMedium.isChecked = true 2 -> priorityLow.isChecked = true @@ -52,49 +45,26 @@ class PriorityControlSet : TaskEditControlFragment() { tintRadioButton(priorityNone, 3) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putInt(EXTRA_PRIORITY, priority) - } - - override val layout: Int - get() = R.layout.control_set_priority + override val layout = R.layout.control_set_priority - override val icon: Int - get() = R.drawable.ic_outline_flag_24px + override val icon = R.drawable.ic_outline_flag_24px override fun controlId() = TAG - override suspend fun apply(task: Task) { - task.priority = priority - } - - override suspend fun hasChanges(original: Task): Boolean { - return original.priority != priority - } - private fun tintRadioButton(radioButton: AppCompatRadioButton, priority: Int) { val color = colorProvider.getPriorityColor(priority, true) radioButton.buttonTintList = ColorStateList(arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)), intArrayOf(color, color)) } @Task.Priority - private fun getPriority(): Int { - if (priorityHigh.isChecked) { - return Task.Priority.HIGH - } - if (priorityMedium.isChecked) { - return Task.Priority.MEDIUM - } - return if (priorityLow.isChecked) { - Task.Priority.LOW - } else { - Task.Priority.NONE - } + private fun getPriority() = when { + priorityHigh.isChecked -> Task.Priority.HIGH + priorityMedium.isChecked -> Task.Priority.MEDIUM + priorityLow.isChecked -> Task.Priority.LOW + else -> Task.Priority.NONE } companion object { const val TAG = R.string.TEA_ctrl_importance_pref - private const val EXTRA_PRIORITY = "extra_priority" } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt index 17e2a9096..2d29df5ae 100644 --- a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt +++ b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.graphics.Paint import android.os.Bundle +import android.text.Editable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,6 +14,7 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.LinearLayout +import androidx.core.widget.addTextChangedListener import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -24,9 +26,8 @@ import butterknife.OnClick import com.todoroo.andlib.sql.Criterion import com.todoroo.andlib.sql.Join import com.todoroo.andlib.sql.QueryTemplate -import com.todoroo.andlib.utility.DateUtilities +import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.astrid.activity.MainActivity -import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.dao.TaskDao @@ -39,19 +40,19 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.tasks.LocalBroadcastManager import org.tasks.R -import org.tasks.Strings.isNullOrEmpty -import org.tasks.data.* +import org.tasks.data.CaldavDao +import org.tasks.data.GoogleTask +import org.tasks.data.GoogleTaskDao +import org.tasks.data.TaskContainer import org.tasks.locale.Locale import org.tasks.tasklist.SubtaskViewHolder import org.tasks.tasklist.SubtasksRecyclerAdapter -import java.util.* import javax.inject.Inject @AndroidEntryPoint class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks { - @JvmField @BindView(R.id.recycler_view) - var recyclerView: RecyclerView? = null + lateinit var recyclerView: RecyclerView @BindView(R.id.new_subtasks) lateinit var newSubtaskContainer: LinearLayout @@ -68,102 +69,43 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks @Inject lateinit var checkBoxProvider: CheckBoxProvider @Inject lateinit var chipProvider: ChipProvider - private lateinit var viewModel: TaskListViewModel + private lateinit var listViewModel: TaskListViewModel private val refreshReceiver = RefreshReceiver() private var remoteList: Filter? = null private var googleTask: GoogleTask? = null private lateinit var recyclerAdapter: SubtasksRecyclerAdapter - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelableArrayList(EXTRA_NEW_SUBTASKS, newSubtasks) - } - override fun createView(savedInstanceState: Bundle?) { - viewModel = ViewModelProvider(this).get(TaskListViewModel::class.java) - if (savedInstanceState != null) { - for (task in savedInstanceState.getParcelableArrayList(EXTRA_NEW_SUBTASKS)!!) { - addSubtask(task) - } - } + listViewModel = ViewModelProvider(this).get(TaskListViewModel::class.java) + viewModel.newSubtasks.forEach { addSubtask(it) } recyclerAdapter = SubtasksRecyclerAdapter(activity, chipProvider, checkBoxProvider, this) - if (task.id > 0) { - recyclerAdapter.submitList(viewModel.value) - viewModel.setFilter(Filter("subtasks", getQueryTemplate(task))) - (recyclerView!!.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false - recyclerView!!.layoutManager = LinearLayoutManager(activity) - recyclerView!!.isNestedScrollingEnabled = false - viewModel.observe(this, Observer { list: List? -> recyclerAdapter.submitList(list) }) - recyclerView!!.adapter = recyclerAdapter + viewModel.task?.let { + if (it.id > 0) { + recyclerAdapter.submitList(listViewModel.value) + listViewModel.setFilter(Filter("subtasks", getQueryTemplate(it))) + (recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false + recyclerView.layoutManager = LinearLayoutManager(activity) + recyclerView.isNestedScrollingEnabled = false + listViewModel.observe(this, Observer { list: List? -> recyclerAdapter.submitList(list) }) + recyclerView.adapter = recyclerAdapter + } } } - override val layout: Int - get() = R.layout.control_set_subtasks + override val layout = R.layout.control_set_subtasks - override val icon: Int - get() = R.drawable.ic_subdirectory_arrow_right_black_24dp + override val icon = R.drawable.ic_subdirectory_arrow_right_black_24dp override fun controlId() = TAG - override fun requiresId() = true - - override suspend fun apply(task: Task) { - for (subtask in newSubtasks) { - if (isNullOrEmpty(subtask.title)) { - continue - } - subtask.completionDate = task.completionDate - taskDao.createNew(subtask) - when (remoteList) { - is GtasksFilter -> { - val googleTask = GoogleTask(subtask.id, (remoteList as GtasksFilter).remoteId) - googleTask.parent = task.id - googleTask.isMoved = true - googleTaskDao.insertAndShift(googleTask, false) - } - is CaldavFilter -> { - val caldavTask = CaldavTask(subtask.id, (remoteList as CaldavFilter).uuid) - subtask.parent = task.id - caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.id) - taskDao.save(subtask) - caldavDao.insert(subtask, caldavTask, false) - } - else -> { - subtask.parent = task.id - taskDao.save(subtask) - } - } - } - } - - override suspend fun hasChanges(original: Task): Boolean { - return newSubtasks.isNotEmpty() - } - - private val newSubtasks: ArrayList - get() { - val subtasks = ArrayList() - val children = newSubtaskContainer.childCount - for (i in 0 until children) { - val view = newSubtaskContainer.getChildAt(i) as ViewGroup - val title = view.getChildAt(2) as EditText - val completed: CheckableImageView = view.findViewById(R.id.completeBox) - val subtask = taskCreator.createWithValues(title.text.toString()) - if (completed.isChecked) { - subtask.completionDate = DateUtilities.now() - } - subtasks.add(subtask) - } - return subtasks - } - override fun onResume() { super.onResume() localBroadcastManager.registerRefreshReceiver(refreshReceiver) lifecycleScope.launch { - googleTask = googleTaskDao.getByTaskId(task.id) - updateUI() + viewModel.task?.let { + googleTask = googleTaskDao.getByTaskId(it.id) + updateUI() + } } } @@ -178,7 +120,9 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks toaster.longToast(R.string.subtasks_multilevel_google_task) return } - val editText = addSubtask(taskCreator.createWithValues("")) + val task = taskCreator.createWithValues("") + viewModel.newSubtasks.add(task) + val editText = addSubtask(task) editText.requestFocus() val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) @@ -195,6 +139,9 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks editText.maxLines = Int.MAX_VALUE editText.isFocusable = true editText.isEnabled = true + editText.addTextChangedListener { text: Editable? -> + task.title = text?.toString() + } editText.setOnEditorActionListener { _, actionId: Int, _ -> if (actionId == EditorInfo.IME_ACTION_NEXT) { if (editText.text.isNotEmpty()) { @@ -214,6 +161,7 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks private fun updateCompleteBox(task: Task, completeBox: CheckableImageView, editText: EditText) { val isComplete = completeBox.isChecked + task.completionDate = if (isComplete) now() else 0 completeBox.setImageDrawable( checkBoxProvider.getCheckBox(isComplete, false, task.priority)) editText.paintFlags = if (isComplete) { @@ -229,10 +177,10 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks private fun updateUI() { if (isGoogleTaskChild) { - recyclerView!!.visibility = View.GONE + recyclerView.visibility = View.GONE newSubtaskContainer.visibility = View.GONE } else { - recyclerView!!.visibility = View.VISIBLE + recyclerView.visibility = View.VISIBLE newSubtaskContainer.visibility = View.VISIBLE recyclerAdapter.setMultiLevelSubtasksEnabled(remoteList !is GtasksFilter) refresh() @@ -241,13 +189,11 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks fun onRemoteListChanged(filter: Filter?) { remoteList = filter - if (recyclerView != null) { - updateUI() - } + updateUI() } private fun refresh() { - viewModel.invalidate() + listViewModel.invalidate() } override fun openSubtask(task: Task) { @@ -273,7 +219,6 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks companion object { const val TAG = R.string.TEA_ctrl_subtask_pref - private const val EXTRA_NEW_SUBTASKS = "extra_new_subtasks" private fun getQueryTemplate(task: Task): QueryTemplate { return QueryTemplate() .join( diff --git a/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt b/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt index d5166399a..3197c00c4 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt @@ -1,6 +1,5 @@ package org.tasks.ui -import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -8,21 +7,17 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.ViewModelProviders import butterknife.ButterKnife -import com.todoroo.astrid.data.Task -import kotlinx.coroutines.launch import org.tasks.R abstract class TaskEditControlFragment : Fragment() { - protected lateinit var task: Task - - var isNew = false - private set + lateinit var viewModel: TaskEditViewModel override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.control_set_template, null) + viewModel = ViewModelProviders.of(requireParentFragment()).get(TaskEditViewModel::class.java) val content = view.findViewById(R.id.content) inflater.inflate(layout, content) val icon = view.findViewById(R.id.icon) @@ -32,21 +27,12 @@ abstract class TaskEditControlFragment : Fragment() { } ButterKnife.bind(this, view) - lifecycleScope.launch { - createView(savedInstanceState) - } + createView(savedInstanceState) return view } - protected abstract fun createView(savedInstanceState: Bundle?) - - override fun onAttach(activity: Activity) { - super.onAttach(activity) - val args = requireArguments() - task = args.getParcelable(EXTRA_TASK)!! - isNew = args.getBoolean(EXTRA_IS_NEW) - } + protected open fun createView(savedInstanceState: Bundle?) {} protected open fun onRowClick() {} protected open val isClickable: Boolean @@ -55,18 +41,4 @@ abstract class TaskEditControlFragment : Fragment() { protected abstract val layout: Int protected abstract val icon: Int abstract fun controlId(): Int - open fun requiresId(): Boolean { - return false - } - - abstract suspend fun apply(task: Task) - - open suspend fun hasChanges(original: Task): Boolean { - return false - } - - companion object { - const val EXTRA_TASK = "extra_task" - const val EXTRA_IS_NEW = "extra_is_new" - } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt new file mode 100644 index 000000000..126378b62 --- /dev/null +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -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>() + + 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? = null + private set(value) { + field = value + selectedTags = value?.let { ArrayList(it) } + } + + var selectedTags: ArrayList? = null + + var newSubtasks = ArrayList() + + var reminderPeriod: Long? = null + get() = field ?: task?.reminderPeriod ?: 0 + + var originalAlarms: ImmutableSet? = null + private set(value) { + field = value + selectedAlarms = value?.let { HashSet(it) } + } + + var selectedAlarms: HashSet? = 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") + } + } +} \ No newline at end of file diff --git a/app/src/test/java/org/tasks/ui/TaskEditViewModelTest.kt b/app/src/test/java/org/tasks/ui/TaskEditViewModelTest.kt new file mode 100644 index 000000000..6dbb1bcef --- /dev/null +++ b/app/src/test/java/org/tasks/ui/TaskEditViewModelTest.kt @@ -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()) + } +} \ No newline at end of file