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

1032 lines
41 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.Gravity
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
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.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
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.AstridApiConstants.EXTRAS_OLD_DUE_DATE
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.api.IdListFilter
import com.todoroo.astrid.api.SearchFilter
import com.todoroo.astrid.api.TagFilter
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.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskDuplicator
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.timers.TimerPlugin
import com.todoroo.astrid.utility.Flags
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.SortSettingsActivity
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.DragAndDropRecyclerAdapter
import org.tasks.tasklist.TaskViewHolder
import org.tasks.tasklist.ViewHolderFactory
import org.tasks.themes.ColorProvider
import org.tasks.themes.ThemeColor
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskListEvent
import org.tasks.ui.TaskListEventBus
import org.tasks.ui.TaskListViewModel
import java.time.format.FormatStyle
import java.util.Locale
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: DragAndDropRecyclerAdapter? = 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)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
listViewModel.tasks.collect {
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 { _, 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 (recyclerAdapter !is DragAndDropRecyclerAdapter) {
setAdapter(
DragAndDropRecyclerAdapter(
taskAdapter, recyclerView, viewHolderFactory, this, tasks, preferences))
} else {
recyclerAdapter?.submitList(tasks)
}
}
private fun setAdapter(adapter: DragAndDropRecyclerAdapter) {
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 (!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.setFilter(
BuiltInFilterExposer.getMyTasksFilter(requireContext().resources))
} else {
val savedFilter = createSearchFilter(searchQuery!!)
listViewModel.setFilter(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 -> {
requireActivity().startActivityForResult(
SortSettingsActivity.getIntent(
requireActivity(),
filter.supportsManualSort(),
filter.supportsAstridSorting() && preferences.isAstridSortEnabled,
),
REQUEST_SORT
)
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() {
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.isNullOrEmpty() && 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.setFilter(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::id))
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.indent * 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.task, 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.task)
}
}
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: Long, collapsed: Boolean) {
lifecycleScope.launch {
taskDao.setCollapsed(task, 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_DATE_TIME_PICKER = "frag_tag_date_time_picker"
private const val REQUEST_LIST_SETTINGS = 10101
private const val REQUEST_TAG_TASKS = 10106
const val REQUEST_SORT = 10107
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
}
}
}