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

1007 lines
40 KiB
Kotlin

/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.activity
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.speech.RecognizerIntent
import android.view.*
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.setMargins
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.PagedList
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.composethemeadapter.MdcTheme
import com.google.android.material.snackbar.Snackbar
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider
import com.todoroo.astrid.api.*
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.repeats.RepeatTaskHelper
import com.todoroo.astrid.service.*
import com.todoroo.astrid.timers.TimerPlugin
import com.todoroo.astrid.utility.Flags
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.ShortcutManager
import org.tasks.Tasks.Companion.IS_GOOGLE_PLAY
import org.tasks.activities.FilterSettingsActivity
import org.tasks.activities.GoogleTaskListSettingsActivity
import org.tasks.activities.PlaceSettingsActivity
import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase
import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.SubscriptionNagBanner
import org.tasks.data.CaldavDao
import org.tasks.data.TagDataDao
import org.tasks.data.TaskContainer
import org.tasks.databinding.FragmentTaskListBinding
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.FilterPicker.Companion.newFilterPicker
import org.tasks.dialogs.FilterPicker.Companion.setFilterPickerResultListener
import org.tasks.dialogs.SortDialog
import org.tasks.extensions.Context.openUri
import org.tasks.extensions.Context.toast
import org.tasks.extensions.Fragment.safeStartActivityForResult
import org.tasks.extensions.formatNumber
import org.tasks.extensions.setOnQueryTextListener
import org.tasks.filters.PlaceFilter
import org.tasks.intents.TaskIntents
import org.tasks.notifications.NotificationManager
import org.tasks.preferences.Device
import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters
import org.tasks.tags.TagPickerActivity
import org.tasks.tasklist.*
import org.tasks.themes.ColorProvider
import org.tasks.themes.ThemeColor
import org.tasks.ui.*
import java.time.format.FormatStyle
import java.util.*
import javax.inject.Inject
import kotlin.math.max
@AndroidEntryPoint
class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickListener,
MenuItem.OnActionExpandListener, SearchView.OnQueryTextListener, ActionMode.Callback,
TaskViewHolder.ViewHolderCallbacks {
private val refreshReceiver = RefreshReceiver()
private val repeatConfirmationReceiver = RepeatConfirmationReceiver()
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var timerPlugin: TimerPlugin
@Inject lateinit var viewHolderFactory: ViewHolderFactory
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var device: Device
@Inject lateinit var taskMover: TaskMover
@Inject lateinit var taskAdapterProvider: TaskAdapterProvider
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDuplicator: TaskDuplicator
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var defaultThemeColor: ThemeColor
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var shortcutManager: ShortcutManager
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var locale: Locale
@Inject lateinit var firebase: Firebase
@Inject lateinit var repeatTaskHelper: RepeatTaskHelper
@Inject lateinit var taskListEventBus: TaskListEventBus
@Inject lateinit var taskEditEventBus: TaskEditEventBus
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private lateinit var emptyRefreshLayout: SwipeRefreshLayout
private lateinit var coordinatorLayout: CoordinatorLayout
private lateinit var recyclerView: RecyclerView
private val listViewModel: TaskListViewModel by viewModels()
private lateinit var taskAdapter: TaskAdapter
private var recyclerAdapter: TaskListRecyclerAdapter? = null
private lateinit var filter: Filter
private var searchJob: Job? = null
private lateinit var search: MenuItem
private var searchQuery: String? = null
private var mode: ActionMode? = null
lateinit var themeColor: ThemeColor
private lateinit var callbacks: TaskListFragmentCallbackHandler
private lateinit var binding: FragmentTaskListBinding
@OptIn(ExperimentalAnimationApi::class)
private fun process(event: TaskListEvent) = when (event) {
is TaskListEvent.TaskCreated ->
onTaskCreated(event.uuid)
is TaskListEvent.CalendarEventCreated ->
makeSnackbar(R.string.calendar_event_created, event.title)
?.setAction(R.string.action_open) { context?.openUri(event.uri) }
?.show()
is TaskListEvent.BegForSubscription -> {
binding.banner.setContent {
var showBanner by rememberSaveable { mutableStateOf(true) }
MdcTheme {
SubscriptionNagBanner(
visible = showBanner,
subscribe = {
showBanner = false
preferences.lastSubscribeRequest = now()
purchase()
firebase.logEvent(R.string.event_banner_sub, R.string.param_click to true)
},
dismiss = {
showBanner = false
preferences.lastSubscribeRequest = now()
firebase.logEvent(R.string.event_banner_sub, R.string.param_click to false)
},
)
}
}
}
}
private fun purchase() {
if (IS_GOOGLE_PLAY) {
startActivity(Intent(context, PurchaseActivity::class.java))
} else {
preferences.lastSubscribeRequest = now()
context?.openUri(R.string.url_donate)
}
}
override fun onRefresh() {
syncAdapters.sync(true)
lifecycleScope.launch {
delay(1000)
refresh()
}
}
private fun setSyncOngoing() {
AndroidUtilities.assertMainThread()
val ongoing = preferences.isSyncOngoing
swipeRefreshLayout.isRefreshing = ongoing
emptyRefreshLayout.isRefreshing = ongoing
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
if (savedInstanceState != null) {
val longArray = savedInstanceState.getLongArray(EXTRA_SELECTED_TASK_IDS)
if (longArray?.isNotEmpty() == true) {
taskAdapter.setSelected(longArray.toList())
startActionMode()
}
}
}
override fun onAttach(activity: Activity) {
super.onAttach(activity)
callbacks = activity as TaskListFragmentCallbackHandler
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val selectedTaskIds: List<Long> = taskAdapter.getSelected()
outState.putLongArray(EXTRA_SELECTED_TASK_IDS, selectedTaskIds.toLongArray())
outState.putString(EXTRA_SEARCH, searchQuery)
outState.putLongArray(EXTRA_COLLAPSED, taskAdapter.getCollapsed().toLongArray())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
taskListEventBus
.onEach(this::process)
.launchIn(viewLifecycleOwner.lifecycleScope)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentTaskListBinding.inflate(inflater, container, false)
filter = getFilter()
with (binding) {
swipeRefreshLayout = bodyStandard.swipeLayout
emptyRefreshLayout = bodyEmpty.swipeLayoutEmpty
coordinatorLayout = taskListCoordinator
recyclerView = bodyStandard.recyclerView
fab.setOnClickListener { createNewTask() }
fab.isVisible = filter.isWritable
}
themeColor = if (filter.tint != 0) colorProvider.getThemeColor(filter.tint, true) else defaultThemeColor
filter.setFilterQueryOverride(null)
// set up list adapters
taskAdapter = taskAdapterProvider.createTaskAdapter(filter)
taskAdapter.setCollapsed(savedInstanceState?.getLongArray(EXTRA_COLLAPSED))
if (savedInstanceState != null) {
searchQuery = savedInstanceState.getString(EXTRA_SEARCH)
}
listViewModel.setFilter((if (searchQuery == null) filter else createSearchFilter(searchQuery!!)))
(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView.layoutManager = LinearLayoutManager(context)
listViewModel.observe(this) {
submitList(it)
if (it.isEmpty()) {
swipeRefreshLayout.visibility = View.GONE
emptyRefreshLayout.visibility = View.VISIBLE
} else {
swipeRefreshLayout.visibility = View.VISIBLE
emptyRefreshLayout.visibility = View.GONE
}
}
setupRefresh(swipeRefreshLayout)
setupRefresh(emptyRefreshLayout)
binding.toolbar.title = filter.listingTitle
binding.appbarlayout.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
if (verticalOffset == 0 && binding.bottomAppBar.isScrolledDown) {
binding.bottomAppBar.performShow()
}
})
val toolbar = if (preferences.isTopAppBar) {
binding.bottomAppBar.isVisible = false
with (binding.fab) {
layoutParams = (layoutParams as CoordinatorLayout.LayoutParams).apply {
setMargins(resources.getDimensionPixelSize(R.dimen.keyline_first))
anchorId = View.NO_ID
gravity = Gravity.BOTTOM or Gravity.END
}
}
binding.toolbar.setNavigationIcon(R.drawable.ic_outline_menu_24px)
binding.toolbar
} else {
themeColor.apply(binding.bottomAppBar)
binding.bottomAppBar.isVisible = true
binding.toolbar.navigationIcon = null
binding.bottomAppBar
}
if (!preferences.getBoolean(R.string.p_app_bar_collapse, true)) {
binding.bottomAppBar.hideOnScroll = false
(binding.toolbar.layoutParams as AppBarLayout.LayoutParams).scrollFlags = 0
}
toolbar.setOnMenuItemClickListener(this)
toolbar.setNavigationOnClickListener { callbacks.onNavigationIconClicked() }
setupMenu(toolbar)
childFragmentManager.setFilterPickerResultListener(this) {
val selected = taskAdapter.getSelected()
lifecycleScope.launch {
taskMover.move(selected, it)
}
finishActionMode()
}
return binding.root
}
private fun submitList(tasks: List<TaskContainer>) {
if (tasks is PagedList<TaskContainer>) {
if (recyclerAdapter !is PagedListRecyclerAdapter) {
setAdapter(
PagedListRecyclerAdapter(
taskAdapter, recyclerView, viewHolderFactory, this, tasks, preferences))
return
}
} else if (recyclerAdapter !is DragAndDropRecyclerAdapter) {
setAdapter(
DragAndDropRecyclerAdapter(
taskAdapter, recyclerView, viewHolderFactory, this, tasks, preferences))
return
}
recyclerAdapter?.submitList(tasks)
}
private fun setAdapter(adapter: TaskListRecyclerAdapter) {
recyclerAdapter = adapter
recyclerView.adapter = adapter
taskAdapter.setDataSource(adapter)
}
private fun setupMenu(appBar: Toolbar) {
val menu = appBar.menu
menu.clear()
if (filter.hasBeginningMenu()) {
appBar.inflateMenu(filter.beginningMenu)
}
appBar.inflateMenu(R.menu.menu_task_list_fragment_bottom)
if (filter.hasMenu()) {
appBar.inflateMenu(filter.menu)
}
if (appBar is BottomAppBar) {
menu.removeItem(R.id.menu_search)
}
val hidden = menu.findItem(R.id.menu_show_unstarted)
val completed = menu.findItem(R.id.menu_show_completed)
if (!taskAdapter.supportsHiddenTasks() || !filter.supportsHiddenTasks()) {
completed.isChecked = true
completed.isEnabled = false
hidden.isChecked = true
hidden.isEnabled = false
} else {
hidden.isChecked = preferences.showHidden
completed.isChecked = preferences.showCompleted
}
val sortMenu = menu.findItem(R.id.menu_sort)
if (!filter.supportsSorting()) {
sortMenu.isEnabled = false
sortMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
if (preferences.usePagedQueries()
|| !filter.supportsSubtasks()
|| taskAdapter.supportsAstridSorting()) {
menu.findItem(R.id.menu_collapse_subtasks).isVisible = false
menu.findItem(R.id.menu_expand_subtasks).isVisible = false
}
menu.findItem(R.id.menu_voice_add).isVisible = device.voiceInputAvailable() && filter.isWritable
search = binding.toolbar.menu.findItem(R.id.menu_search).setOnActionExpandListener(this)
menu.findItem(R.id.menu_clear_completed).isVisible = filter.isWritable
}
private fun openFilter(filter: Filter?) {
if (filter == null) {
startActivity(TaskIntents.getTaskListByIdIntent(context, null))
} else {
startActivity(TaskIntents.getTaskListIntent(context, filter))
}
}
private fun searchByQuery(query: String?) {
searchJob?.cancel()
searchJob = lifecycleScope.launch {
delay(SEARCH_DEBOUNCE_TIMEOUT)
searchQuery = query?.trim { it <= ' ' } ?: ""
if (searchQuery?.isEmpty() == true) {
listViewModel.searchByFilter(
BuiltInFilterExposer.getMyTasksFilter(requireContext().resources))
} else {
val savedFilter = createSearchFilter(searchQuery!!)
listViewModel.searchByFilter(savedFilter)
}
}
}
private fun createSearchFilter(query: String): Filter {
return SearchFilter(getString(R.string.FLA_search_filter, query), query)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_voice_add -> {
safeStartActivityForResult(
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
putExtra(
RecognizerIntent.EXTRA_PROMPT,
getString(R.string.voice_create_prompt)
)
},
VOICE_RECOGNITION_REQUEST_CODE
)
true
}
R.id.menu_sort -> {
SortDialog.newSortDialog(filter)
.show(childFragmentManager, FRAG_TAG_SORT_DIALOG)
true
}
R.id.menu_show_unstarted -> {
item.isChecked = !item.isChecked
preferences.showHidden = item.isChecked
loadTaskListContent()
localBroadcastManager.broadcastRefresh()
true
}
R.id.menu_show_completed -> {
item.isChecked = !item.isChecked
preferences.showCompleted = item.isChecked
loadTaskListContent()
localBroadcastManager.broadcastRefresh()
true
}
R.id.menu_clear_completed -> {
dialogBuilder
.newDialog(R.string.clear_completed_tasks_confirmation)
.setPositiveButton(R.string.ok) { _, _ -> clearCompleted() }
.setNegativeButton(R.string.cancel, null)
.show()
true
}
R.id.menu_filter_settings -> {
val filterSettings = Intent(activity, FilterSettingsActivity::class.java)
filterSettings.putExtra(FilterSettingsActivity.TOKEN_FILTER, filter)
startActivityForResult(filterSettings, REQUEST_LIST_SETTINGS)
true
}
R.id.menu_caldav_list_fragment -> {
val calendar = (filter as CaldavFilter).calendar
lifecycleScope.launch {
val account = caldavDao.getAccountByUuid(calendar.account!!)
val caldavSettings = Intent(activity, account!!.listSettingsClass())
.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT, account)
.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_CALENDAR, calendar)
startActivityForResult(caldavSettings, REQUEST_LIST_SETTINGS)
}
true
}
R.id.menu_location_settings -> {
val place = (filter as PlaceFilter).place
val intent = Intent(activity, PlaceSettingsActivity::class.java)
intent.putExtra(PlaceSettingsActivity.EXTRA_PLACE, place as Parcelable)
startActivityForResult(intent, REQUEST_LIST_SETTINGS)
true
}
R.id.menu_gtasks_list_settings -> {
val gtasksSettings = Intent(activity, GoogleTaskListSettingsActivity::class.java)
gtasksSettings.putExtra(
GoogleTaskListSettingsActivity.EXTRA_STORE_DATA, (filter as GtasksFilter).list)
startActivityForResult(gtasksSettings, REQUEST_LIST_SETTINGS)
true
}
R.id.menu_tag_settings -> {
val tagSettings = Intent(activity, TagSettingsActivity::class.java)
tagSettings.putExtra(TagSettingsActivity.EXTRA_TAG_DATA, (filter as TagFilter).tagData)
startActivityForResult(tagSettings, REQUEST_LIST_SETTINGS)
true
}
R.id.menu_expand_subtasks -> {
lifecycleScope.launch {
taskDao.setCollapsed(preferences, filter, false)
localBroadcastManager.broadcastRefresh()
}
true
}
R.id.menu_collapse_subtasks -> {
lifecycleScope.launch {
taskDao.setCollapsed(preferences, filter, true)
localBroadcastManager.broadcastRefresh()
}
true
}
R.id.menu_open_map -> {
(filter as PlaceFilter).openMap(context)
true
}
R.id.menu_share -> {
lifecycleScope.launch {
send(taskDao.fetchTasks(preferences, filter))
}
true
}
else -> onOptionsItemSelected(item)
}
}
private fun clearCompleted() = lifecycleScope.launch {
val count = taskDeleter.clearCompleted(filter)
context?.toast(R.string.delete_multiple_tasks_confirmation, locale.formatNumber(count))
}
private fun createNewTask() {
lifecycleScope.launch {
shortcutManager.reportShortcutUsed(ShortcutManager.SHORTCUT_NEW_TASK)
onTaskListItemClicked(addTask(""))
firebase.addTask("fab")
}
}
private suspend fun addTask(title: String): Task {
return taskCreator.createWithValues(filter, title)
}
private fun setupRefresh(layout: SwipeRefreshLayout) {
layout.setOnRefreshListener(this)
layout.setColorSchemeColors(
colorProvider.getPriorityColor(0, true),
colorProvider.getPriorityColor(1, true),
colorProvider.getPriorityColor(2, true),
colorProvider.getPriorityColor(3, true))
}
override fun onResume() {
super.onResume()
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
localBroadcastManager.registerTaskCompletedReceiver(repeatConfirmationReceiver)
refresh()
}
private fun makeSnackbar(@StringRes res: Int, vararg args: Any?): Snackbar? {
return makeSnackbar(getString(res, *args))
}
private fun makeSnackbar(text: String): Snackbar? = activity?.let {
Snackbar.make(coordinatorLayout, text, 4000)
.setAnchorView(R.id.fab)
.setTextColor(it.getColor(R.color.snackbar_text_color))
.setActionTextColor(it.getColor(R.color.snackbar_action_color))
.apply {
view.setBackgroundColor(it.getColor(R.color.snackbar_background))
}
}
override fun onPause() {
super.onPause()
localBroadcastManager.unregisterReceiver(repeatConfirmationReceiver)
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
fun collapseSearchView(): Boolean {
return (search.isActionViewExpanded
&& (search.collapseActionView() || !search.isActionViewExpanded))
}
private fun refresh() {
loadTaskListContent()
setSyncOngoing()
}
fun loadTaskListContent() {
listViewModel.invalidate()
}
fun getFilter(): Filter {
return requireArguments().getParcelable(EXTRA_FILTER)!!
}
private fun onTaskCreated(tasks: List<Task>) {
for (task in tasks) {
onTaskCreated(task.uuid)
}
syncAdapters.sync()
loadTaskListContent()
}
private fun onTaskCreated(uuid: String) {
lifecycleScope.launch {
taskAdapter.onTaskCreated(uuid)
}
}
private suspend fun onTaskDelete(task: Task) {
taskEditEventBus.emit(TaskEditEvent.Discard(task.id))
timerPlugin.stopTimer(task)
taskAdapter.onTaskDeleted(task)
loadTaskListContent()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
VOICE_RECOGNITION_REQUEST_CODE -> if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch {
val match: List<String>? = data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
if (match != null && match.isNotEmpty() && match[0].isNotEmpty()) {
var recognizedSpeech = match[0]
recognizedSpeech = (recognizedSpeech.substring(0, 1)
.uppercase(Locale.getDefault())
+ recognizedSpeech.substring(1).lowercase(Locale.getDefault()))
onTaskListItemClicked(addTask(recognizedSpeech))
firebase.addTask("voice")
}
}
}
REQUEST_LIST_SETTINGS -> if (resultCode == Activity.RESULT_OK) {
val action = data!!.action
if (ACTION_DELETED == action) {
openFilter(BuiltInFilterExposer.getMyTasksFilter(resources))
} else if (ACTION_RELOAD == action) {
openFilter(data.getParcelableExtra(MainActivity.OPEN_FILTER))
}
}
REQUEST_TAG_TASKS -> if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch {
val modified = tagDataDao.applyTags(
taskDao
.fetch(data!!.getSerializableExtra(TagPickerActivity.EXTRA_TASKS) as ArrayList<Long>)
.filterNot { it.readOnly },
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_PARTIALLY_SELECTED)!!,
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!!
)
taskDao.touch(modified)
}
finishActionMode()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onContextItemSelected(item: MenuItem): Boolean {
return onOptionsItemSelected(item)
}
private fun onTaskListItemClicked(task: Task?) = lifecycleScope.launch {
callbacks.onTaskListItemClicked(task)
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
search.setOnQueryTextListener(this)
if (searchQuery == null) {
searchByQuery("")
}
if (preferences.isTopAppBar) {
binding.toolbar.menu.forEach { it.isVisible = false }
}
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
search.setOnQueryTextListener(null)
listViewModel.searchByFilter(filter)
searchJob?.cancel()
searchQuery = null
if (preferences.isTopAppBar) {
setupMenu(binding.toolbar)
}
return true
}
override fun onQueryTextSubmit(query: String): Boolean {
openFilter(createSearchFilter(query.trim { it <= ' ' }))
search.collapseActionView()
return true
}
override fun onQueryTextChange(query: String): Boolean {
searchByQuery(query)
return true
}
fun broadcastRefresh() {
localBroadcastManager.broadcastRefresh()
}
override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean {
val inflater = actionMode.menuInflater
inflater.inflate(R.menu.menu_multi_select, menu)
if (filter.isReadOnly) {
listOf(R.id.edit_tags, R.id.move_tasks, R.id.reschedule, R.id.copy_tasks, R.id.delete)
.forEach { menu.findItem(it).isVisible = false }
}
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
val selected = taskAdapter.getSelected()
return when (item.itemId) {
R.id.edit_tags -> {
lifecycleScope.launch {
val tags = tagDataDao.getTagSelections(selected)
val intent = Intent(context, TagPickerActivity::class.java)
intent.putExtra(TagPickerActivity.EXTRA_TASKS, selected)
intent.putParcelableArrayListExtra(
TagPickerActivity.EXTRA_PARTIALLY_SELECTED,
ArrayList(tagDataDao.getByUuid(tags.first!!)))
intent.putParcelableArrayListExtra(
TagPickerActivity.EXTRA_SELECTED, ArrayList(tagDataDao.getByUuid(tags.second!!)))
startActivityForResult(intent, REQUEST_TAG_TASKS)
}
true
}
R.id.move_tasks -> {
lifecycleScope.launch {
val singleFilter = taskMover.getSingleFilter(selected)
newFilterPicker(singleFilter, true)
.show(childFragmentManager, FRAG_TAG_REMOTE_LIST_PICKER)
}
true
}
R.id.reschedule -> {
lifecycleScope.launch {
taskDao
.fetch(selected)
.filterNot { it.readOnly }
.takeIf { it.isNotEmpty() }
?.let {
newDateTimePicker(
preferences.getBoolean(R.string.p_auto_dismiss_datetime_list_screen, false),
*it.toTypedArray())
.show(parentFragmentManager, FRAG_TAG_DATE_TIME_PICKER)
}
}
finishActionMode()
true
}
R.id.menu_select_all -> {
lifecycleScope.launch {
taskAdapter.setSelected(taskDao.fetchTasks(preferences, filter)
.map(TaskContainer::getId))
updateModeTitle()
recyclerAdapter?.notifyDataSetChanged()
}
true
}
R.id.menu_share -> {
lifecycleScope.launch {
selected.chunkedMap { taskDao.fetchTasks(preferences, IdListFilter(it)) }
.apply { send(this) }
}
true
}
R.id.delete -> {
dialogBuilder
.newDialog(R.string.delete_selected_tasks)
.setPositiveButton(
R.string.ok) { _, _ -> deleteSelectedItems(selected) }
.setNegativeButton(R.string.cancel, null)
.show()
true
}
R.id.copy_tasks -> {
dialogBuilder
.newDialog(R.string.copy_selected_tasks)
.setPositiveButton(
R.string.ok) { _, _ -> copySelectedItems(selected) }
.setNegativeButton(R.string.cancel, null)
.show()
true
}
else -> false
}
}
private fun send(tasks: List<TaskContainer>) {
val output = tasks.joinToString("\n") { t -> Task
"${(if (t.isCompleted) "☑" else "☐").padStart(1 + t.getIndent() * 3, ' ')} ${t.title}"
}
val intent = ShareCompat
.IntentBuilder(requireContext())
.setType("text/plain")
.setSubject(filter.listingTitle)
.setText(output)
.createChooserIntent()
startActivity(intent)
finishActionMode()
}
override fun onDestroyActionMode(mode: ActionMode) {
this.mode = null
if (taskAdapter.numSelected > 0) {
taskAdapter.clearSelections()
recyclerAdapter?.notifyDataSetChanged()
}
}
private fun showDateTimePicker(task: TaskContainer) {
val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_TIME_PICKER) == null) {
newDateTimePicker(
preferences.getBoolean(R.string.p_auto_dismiss_datetime_list_screen, false),
task.task)
.show(fragmentManager, FRAG_TAG_DATE_TIME_PICKER)
}
}
interface TaskListFragmentCallbackHandler {
suspend fun onTaskListItemClicked(task: Task?)
fun onNavigationIconClicked()
}
val isActionModeActive: Boolean
get() = mode != null
fun startActionMode() {
if (mode == null) {
mode = (activity as AppCompatActivity).startSupportActionMode(this)
updateModeTitle()
Flags.set(Flags.TLFP_NO_INTERCEPT_TOUCH)
}
}
fun finishActionMode() {
mode?.finish()
}
fun updateModeTitle() {
if (mode != null) {
val count = max(1, taskAdapter.numSelected)
mode!!.title = count.toString()
}
}
private fun deleteSelectedItems(tasks: List<Long>) = lifecycleScope.launch {
finishActionMode()
val result = withContext(NonCancellable) {
taskDeleter.markDeleted(tasks)
}
result.forEach { onTaskDelete(it) }
makeSnackbar(R.string.delete_multiple_tasks_confirmation, result.size.toString())?.show()
}
private fun copySelectedItems(tasks: List<Long>) = lifecycleScope.launch {
finishActionMode()
val duplicates = withContext(NonCancellable) {
taskDuplicator.duplicate(tasks)
}
onTaskCreated(duplicates)
makeSnackbar(R.string.copy_multiple_tasks_confirmation, duplicates.size.toString())?.show()
}
fun clearCollapsed() = taskAdapter.clearCollapsed()
override fun onCompletedTask(task: TaskContainer, newState: Boolean) {
if (task.isReadOnly) {
return
}
lifecycleScope.launch {
taskCompleter.setComplete(task.getTask(), newState)
taskAdapter.onCompletedTask(task, newState)
loadTaskListContent()
}
}
override fun onLinkClicked(vh: TaskViewHolder, url: String) =
if (isActionModeActive) {
recyclerAdapter?.toggle(vh)
true
} else {
false
}
override fun onClick(taskViewHolder: TaskViewHolder) {
if (isActionModeActive) {
recyclerAdapter?.toggle(taskViewHolder)
} else {
onTaskListItemClicked(taskViewHolder.task.getTask())
}
}
override fun onClick(filter: Filter) {
if (!isActionModeActive) {
val context = activity
context?.startActivity(TaskIntents.getTaskListIntent(context, filter))
}
}
override fun onLongPress(taskViewHolder: TaskViewHolder): Boolean {
if (recyclerAdapter?.dragAndDropEnabled() != true) {
startActionMode()
}
if (isActionModeActive && !taskViewHolder.moving) {
recyclerAdapter?.toggle(taskViewHolder)
}
return true
}
override fun onChangeDueDate(task: TaskContainer) {
if (task.isReadOnly) {
return
}
showDateTimePicker(task)
}
override fun toggleSubtasks(task: TaskContainer, collapsed: Boolean) {
lifecycleScope.launch {
taskDao.setCollapsed(task.id, collapsed)
}
}
private inner class RefreshReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
refresh()
}
}
private inner class RepeatConfirmationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
lifecycleScope.launch {
val tasks =
(intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList<Long>)
?.let { taskDao.fetch(it) }
?.filterNot { it.readOnly }
?.takeIf { it.isNotEmpty() }
?: return@launch
val isRecurringCompletion =
tasks.size == 1 && tasks.first().let { it.isRecurring && !it.isCompleted }
val oldDueDate = if (isRecurringCompletion) {
intent.getLongExtra(EXTRAS_OLD_DUE_DATE, 0)
} else {
0
}
val undoCompletion = View.OnClickListener {
lifecycleScope.launch {
tasks
.partition { it.isRecurring }
.let { (recurring, notRecurring) ->
recurring.forEach { repeatTaskHelper.undoRepeat(it, oldDueDate) }
taskCompleter.setComplete(notRecurring, 0L)
}
}
}
if (isRecurringCompletion) {
val task = tasks.first()
val text = getString(
R.string.repeat_snackbar,
task.title,
DateUtilities.getRelativeDateTime(
context, task.dueDate, locale, FormatStyle.LONG, true
)
)
makeSnackbar(text)?.setAction(R.string.DLG_undo, undoCompletion)?.show()
} else {
val text = if (tasks.size == 1) {
context.getString(R.string.snackbar_task_completed)
} else {
context.getString(R.string.snackbar_tasks_completed, tasks.size)
}
makeSnackbar(text)?.setAction(R.string.DLG_undo, undoCompletion)?.show()
}
}
}
}
companion object {
const val TAGS_METADATA_JOIN = "for_tags" // $NON-NLS-1$
const val CALDAV_METADATA_JOIN = "for_caldav" // $NON-NLS-1$
const val ACTION_RELOAD = "action_reload"
const val ACTION_DELETED = "action_deleted"
private const val EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids"
private const val EXTRA_SEARCH = "extra_search"
private const val EXTRA_COLLAPSED = "extra_collapsed"
private const val VOICE_RECOGNITION_REQUEST_CODE = 1234
private const val EXTRA_FILTER = "extra_filter"
private const val FRAG_TAG_REMOTE_LIST_PICKER = "frag_tag_remote_list_picker"
private const val FRAG_TAG_SORT_DIALOG = "frag_tag_sort_dialog"
private const val FRAG_TAG_DATE_TIME_PICKER = "frag_tag_date_time_picker"
private const val REQUEST_LIST_SETTINGS = 10101
private const val REQUEST_TAG_TASKS = 10106
private const val SEARCH_DEBOUNCE_TIMEOUT = 300L
fun newTaskListFragment(context: Context, filter: Filter?): TaskListFragment {
val fragment = TaskListFragment()
val bundle = Bundle()
bundle.putParcelable(
EXTRA_FILTER,
filter ?: BuiltInFilterExposer.getMyTasksFilter(context.resources))
fragment.arguments = bundle
return fragment
}
}
}