@ -19,7 +19,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity. OnBackPresse dCallback
import androidx.activity. ad dCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
@ -27,6 +27,7 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.ui.platform.LocalContext
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat
import androidx.core.content.IntentCompat
@ -34,6 +35,7 @@ import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.setMargins
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
@ -41,14 +43,15 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.room.withTransaction
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 .sql.Join
import com.todoroo.andlib .sql.QueryTemplate
import org.tasks.data .sql.Join
import org.tasks.data .sql.QueryTemplate
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider
@ -63,11 +66,9 @@ import com.todoroo.astrid.api.GtasksFilter
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
@ -77,24 +78,30 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.ShortcutManager
import org.tasks.Tasks
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.compose.collectAsStateLifecycleAware
import org.tasks.data.CaldavDao
import org.tasks.data.Tag
import org.tasks.data.TagDataDao
import org.tasks.data.dao.CaldavDao
import org.tasks.data.db.Database
import org.tasks.data.entity.Tag
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Task
import org.tasks.data.TaskContainer
import org.tasks.data.listSettingsClass
import org.tasks.databinding.FragmentTaskListBinding
import org.tasks.d b.SuspendDbUtils.chunkedMap
import org.tasks.d ata.d b.SuspendDbUtils.chunkedMap
import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.FilterPicker.Companion.newFilterPicker
@ -104,19 +111,21 @@ 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. hideKeyboard
import org.tasks.extensions.setOnQueryTextListener
import org.tasks.filters.PlaceFilter
import org.tasks. intents.TaskIntents
import org.tasks. markdown.MarkdownProvider
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.SectionedDataSource
import org.tasks.tasklist.TaskViewHolder
import org.tasks.tasklist.ViewHolderFactory
import org.tasks.themes.ColorProvider
import org.tasks.themes.ThemeColor
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskListEvent
@ -135,7 +144,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
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
@ -158,21 +166,18 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
@Inject lateinit var repeatTaskHelper : RepeatTaskHelper
@Inject lateinit var taskListEventBus : TaskListEventBus
@Inject lateinit var taskEditEventBus : TaskEditEventBus
@Inject lateinit var database : Database
@Inject lateinit var markdown : MarkdownProvider
private val listViewModel : TaskListViewModel by viewModels ( )
private val mainViewModel : MainActivityViewModel by activityViewModels ( )
private lateinit var taskAdapter : TaskAdapter
private var recyclerAdapter : DragAndDropRecyclerAdapter ? = null
private lateinit var filter : Filter
private lateinit var search : MenuItem
private var mode : ActionMode ? = null
lateinit var themeColor : ThemeColor
private lateinit var callbacks : TaskListFragmentCallbackHandler
private lateinit var binding : FragmentTaskListBinding
private val onBackPressed = object : OnBackPressedCallback ( false ) {
override fun handleOnBackPressed ( ) {
search . collapseActionView ( )
}
}
private val sortRequest =
registerForActivityResult ( ActivityResultContracts . StartActivityForResult ( ) ) { result ->
@ -182,7 +187,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
activity ?. recreate ( )
}
if ( data . getBooleanExtra ( SortSettingsActivity . EXTRA _CHANGED _GROUP , false ) ) {
taskAdapter . clearCollapsed ( )
listViewModel . clearCollapsed ( )
}
listViewModel . invalidate ( )
}
@ -195,10 +200,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
val data = result . data ?: return @registerForActivityResult
when ( data . action ) {
ACTION _DELETED ->
open Filter( BuiltInFilterExposer . getMyTasksFilter ( resources ) )
mainViewModel. set Filter( BuiltInFilterExposer . getMyTasksFilter ( resources ) )
ACTION _RELOAD ->
IntentCompat . getParcelableExtra ( data , MainActivity . OPEN _FILTER , Filter :: class . java ) ?. let {
open Filter( it )
mainViewModel. set Filter( it )
}
}
}
@ -227,16 +232,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
}
override fun onAttach ( context : Context ) {
super . onAttach ( requireContext ( ) )
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 . putLongArray ( EXTRA _COLLAPSED , taskAdapter . getCollapsed ( ) . toLongArray ( ) )
}
override fun onViewCreated ( view : View , savedInstanceState : Bundle ? ) {
@ -247,15 +246,21 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
. launchIn ( viewLifecycleOwner . lifecycleScope )
}
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
requireActivity ( ) . onBackPressedDispatcher . addCallback ( requireActivity ( ) , onBackPressed )
}
@OptIn ( ExperimentalAnimationApi :: class )
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ? , savedInstanceState : Bundle ? ) : View {
requireActivity ( ) . onBackPressedDispatcher . addCallback ( owner = viewLifecycleOwner ) {
if ( search . isActionViewExpanded ) {
search . collapseActionView ( )
} else {
requireActivity ( ) . finish ( )
if ( ! preferences . getBoolean ( R . string . p _open _last _viewed _list , true ) ) {
runBlocking {
mainViewModel . resetFilter ( )
}
}
}
}
binding = FragmentTaskListBinding . inflate ( inflater , container , false )
filter = getFilter ( )
val swipeRefreshLayout : SwipeRefreshLayout
@ -273,23 +278,24 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
// set up list adapters
taskAdapter = taskAdapterProvider . createTaskAdapter ( filter )
taskAdapter . setCollapsed ( savedInstanceState ?. getLongArray ( EXTRA _COLLAPSED ) )
listViewModel . setFilter ( filter )
( recyclerView . itemAnimator as DefaultItemAnimator ) . supportsChangeAnimations = false
recyclerView . layoutManager = LinearLayoutManager ( context )
lifecycleScope . launch {
viewLifecycleOwner . repeatOnLifecycle ( Lifecycle . State . STARTED ) {
listViewModel . state . collect {
submitList ( it . tasks )
if ( it . tasks . isEmpty ( ) ) {
swipeRefreshLayout . visibility = View . GONE
emptyRefreshLayout . visibility = View . VISIBLE
} else {
swipeRefreshLayout . visibility = View . VISIBLE
emptyRefreshLayout . visibility = View . GONE
if ( it . tasks is TaskListViewModel . TasksResults . Results ) {
submitList ( it . tasks . tasks )
if ( it . tasks . tasks . isEmpty ( ) ) {
swipeRefreshLayout . visibility = View . GONE
emptyRefreshLayout . visibility = View . VISIBLE
} else {
swipeRefreshLayout . visibility = View . VISIBLE
emptyRefreshLayout . visibility = View . GONE
}
swipeRefreshLayout . isRefreshing = it . syncOngoing
emptyRefreshLayout . isRefreshing = it . syncOngoing
}
swipeRefreshLayout . isRefreshing = it . syncOngoing
emptyRefreshLayout . isRefreshing = it . syncOngoing
}
}
}
@ -323,7 +329,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
( binding . toolbar . layoutParams as AppBarLayout . LayoutParams ) . scrollFlags = 0
}
toolbar . setOnMenuItemClickListener ( this )
toolbar . setNavigationOnClickListener { callbacks . onNavigationIconClicked ( ) }
toolbar . setNavigationOnClickListener {
activity ?. hideKeyboard ( )
mainViewModel . setDrawerOpen ( true )
}
setupMenu ( toolbar )
childFragmentManager . setFilterPickerResultListener ( this ) {
val selected = taskAdapter . getSelected ( )
@ -333,23 +342,42 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
finishActionMode ( )
}
binding . banner . setContent {
val context = LocalContext . current
val showBanner = listViewModel . state . collectAsStateLifecycleAware ( ) . value . begForSubscription
MdcTheme {
SubscriptionNagBanner (
visible = showBanner ,
subscribe = { listViewModel . dismissBanner ( clickedPurchase = true ) } ,
dismiss = { listViewModel . dismissBanner ( clickedPurchase = false ) } ,
subscribe = {
listViewModel . dismissBanner ( clickedPurchase = true )
if ( Tasks . IS _GOOGLE _PLAY ) {
context . startActivity ( Intent ( context , PurchaseActivity :: class . java ) )
} else {
preferences . lastSubscribeRequest = currentTimeMillis ( )
context . openUri ( R . string . url _donate )
}
} ,
dismiss = {
listViewModel . dismissBanner ( clickedPurchase = false )
} ,
)
}
}
return binding . root
}
private fun submitList ( tasks : List < TaskContainer > ) {
private fun submitList ( tasks : SectionedDataSource ) {
if ( recyclerAdapter !is DragAndDropRecyclerAdapter ) {
setAdapter (
DragAndDropRecyclerAdapter (
taskAdapter , binding . bodyStandard . recyclerView , viewHolderFactory , this , tasks , preferences ) )
adapter = taskAdapter ,
recyclerView = binding . bodyStandard . recyclerView ,
viewHolderFactory = viewHolderFactory ,
taskList = this ,
tasks = tasks ,
preferences = preferences ,
toggleCollapsed = { listViewModel . toggleCollapsed ( it ) } ,
)
)
} else {
recyclerAdapter ?. submitList ( tasks )
}
@ -402,21 +430,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
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 ) . also {
it . setOnActionExpandListener ( this )
it . setOnQueryTextListener ( this )
}
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 ) )
}
}
override fun onMenuItemClick ( item : MenuItem ) : Boolean {
return when ( item . itemId ) {
R . id . menu _voice _add -> {
@ -459,11 +476,28 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
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 ( )
lifecycleScope . launch {
val tasks = listViewModel . getTasksToClear ( )
val countString = requireContext ( ) . resources . getQuantityString ( R . plurals . Ntasks , tasks . size , tasks . size )
if ( tasks . isEmpty ( ) ) {
context ?. toast ( R . string . delete _multiple _tasks _confirmation , countString )
} else {
dialogBuilder
. newDialog ( R . string . clear _completed _tasks _confirmation )
. setMessage ( R . string . clear _completed _tasks _count , countString )
. setPositiveButton ( R . string . ok ) { _ , _ ->
lifecycleScope . launch {
listViewModel . markDeleted ( tasks )
context ?. toast (
R . string . delete _multiple _tasks _confirmation ,
countString
)
}
}
. setNegativeButton ( R . string . cancel , null )
. show ( )
}
}
true
}
R . id . menu _filter _settings -> {
@ -535,11 +569,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
}
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 )
@ -647,11 +676,11 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
private fun onTaskListItemClicked ( task : Task ? ) = lifecycleScope . launch {
callbacks. onTaskListItemClicked ( task )
mainViewModel. setTask ( task )
}
override fun onMenuItemActionExpand ( item : MenuItem ) : Boolean {
onBackPressed. isEnabled = true
search. setOnQueryTextListener ( this )
listViewModel . setSearchQuery ( " " )
if ( preferences . isTopAppBar ) {
binding . toolbar . menu . forEach { it . isVisible = false }
@ -660,7 +689,8 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
override fun onMenuItemActionCollapse ( item : MenuItem ) : Boolean {
onBackPressed . isEnabled = false
search . setOnQueryTextListener ( null )
listViewModel . setFilter ( filter )
listViewModel . setSearchQuery ( null )
if ( preferences . isTopAppBar ) {
setupMenu ( binding . toolbar )
@ -669,7 +699,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
override fun onQueryTextSubmit ( query : String ) : Boolean {
open Filter( requireContext ( ) . createSearchQuery ( query . trim ( ) ) )
mainViewModel. set Filter( requireContext ( ) . createSearchQuery ( query . trim ( ) ) )
search . collapseActionView ( )
return true
}
@ -826,11 +856,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
}
interface TaskListFragmentCallbackHandler {
suspend fun onTaskListItemClicked ( task : Task ? )
fun onNavigationIconClicked ( )
}
val isActionModeActive : Boolean
get ( ) = mode != null
@ -857,7 +882,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
finishActionMode ( )
val result = withContext ( NonCancellable ) {
taskDeleter . markDeleted ( tasks )
listViewModel . markDeleted ( tasks )
}
result . forEach { onTaskDelete ( it ) }
makeSnackbar ( R . string . delete _multiple _tasks _confirmation , result . size . toString ( ) ) ?. show ( )
@ -878,15 +903,13 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
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 )
taskAdapter . onCompletedTask ( task .uuid , newState )
loadTaskListContent ( )
}
}
@ -909,8 +932,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onClick ( filter : Filter ) {
if ( !is ActionModeActive ) {
val context = activity
context ?. startActivity ( TaskIntents . getTaskListIntent ( context , filter ) )
mainViewModel . setFilter ( filter )
}
}
@ -942,7 +964,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
lifecycleScope . launch {
val tasks =
( intent . getSerializableExtra ( EXTRAS _TASK _ID ) as ? ArrayList < Long > )
?. let { taskDao . fetch ( it ) }
?. let {
// hack to wait for task save transaction to complete
database . withTransaction {
taskDao . fetch ( it )
}
}
?. filterNot { it . readOnly }
?. takeIf { it . isNotEmpty ( ) }
?: return @launch
@ -965,9 +992,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
if ( isRecurringCompletion ) {
val task = tasks . first ( )
val title = markdown . markdown ( force = true ) . toMarkdown ( task . title )
val text = getString (
R . string . repeat _snackbar ,
t ask. t itle,
t itle,
DateUtilities . getRelativeDateTime (
context , task . dueDate , locale , FormatStyle . LONG , true
)
@ -989,19 +1017,17 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
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 _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 FRAG _TAG _PRIORITY _PICKER = " frag_tag_priority_picker "
private const val REQUEST _TAG _TASKS = 10106
fun newTaskListFragment ( context : Context , filter : Filter ? ) : TaskListFragment {
fun newTaskListFragment ( filter : Filter ) : TaskListFragment {
val fragment = TaskListFragment ( )
val bundle = Bundle ( )
bundle . putParcelable (
EXTRA _FILTER ,
filter ?: BuiltInFilterExposer . getMyTasksFilter ( context . resources ) )
bundle . putParcelable ( EXTRA _FILTER , filter )
fragment . arguments = bundle
return fragment
}