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/org/tasks/widget/ScrollableViewsFactory.kt

395 lines
16 KiB
Kotlin

package org.tasks.widget
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Paint
import android.view.View
import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.AstridOrderingFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.core.SortHelper
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.subtasks.SubtasksHelper
import kotlinx.coroutines.runBlocking
import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.date.DateTimeUtils
import org.tasks.extensions.Context.isNightMode
import org.tasks.markdown.Markdown
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import org.tasks.tasklist.SectionedDataSource
import org.tasks.tasklist.SectionedDataSource.Companion.HEADER_COMPLETED
import org.tasks.time.DateTimeUtils.startOfDay
import org.tasks.ui.CheckBoxProvider
import timber.log.Timber
import java.time.format.FormatStyle
import java.util.Locale
import kotlin.math.max
internal class ScrollableViewsFactory(
private val subtasksHelper: SubtasksHelper,
preferences: Preferences,
private val context: Context,
private val widgetId: Int,
private val taskDao: TaskDao,
private val defaultFilterProvider: DefaultFilterProvider,
private val checkBoxProvider: CheckBoxProvider,
private val locale: Locale,
private val chipProvider: ChipProvider,
private val localBroadcastManager: LocalBroadcastManager,
private val markdown: Markdown,
private val headerFormatter: HeaderFormatter,
) : RemoteViewsFactory {
private val indentPadding: Int
private var showDueDates = false
private var endDueDate = false
private var showCheckboxes = false
private var textSize = 0f
private var dueDateTextSize = 0f
private var filter: Filter? = null
private var textColorPrimary = 0
private var textColorSecondary = 0
private var showFullTaskTitle = false
private var showDescription = false
private var showFullDescription = false
private var vPad = 0
private var hPad = 0
private var handleDueDateClick = false
private var showDividers = false
private var disableGroups = false
private var showSubtasks = false
private var showStartDates = false
private var showPlaces = false
private var showLists = false
private var showTags = false
private var collapsed = mutableSetOf(HEADER_COMPLETED)
private var groupMode = -1
private var subtaskMode = -1
private var tasks = SectionedDataSource(
emptyList(),
disableHeaders = false,
groupMode = SortHelper.GROUP_NONE,
subtaskMode = SortHelper.SORT_MANUAL,
collapsed,
preferences.completedTasksAtBottom,
)
private val widgetPreferences = WidgetPreferences(context, preferences, widgetId)
private var isDark = checkIfDark
private var showFullDate = false
private var compact = false
private val checkIfDark: Boolean
get() = when (widgetPreferences.themeIndex) {
0 -> false
3 -> context.isNightMode
else -> true
}
override fun onCreate() {}
override fun onDataSetChanged() {
runBlocking {
updateSettings()
tasks = SectionedDataSource(
taskDao.fetchTasks { getQuery(filter) },
disableGroups,
groupMode,
subtaskMode,
collapsed,
widgetPreferences.completedTasksAtBottom,
)
if (collapsed.retainAll(tasks.getSectionValues())) {
widgetPreferences.collapsed = collapsed
}
}
}
override fun onDestroy() {}
override fun getCount(): Int {
if (isDark != checkIfDark) {
isDark = !isDark
localBroadcastManager.reconfigureWidget(widgetId)
}
return tasks.size
}
override fun getViewAt(position: Int): RemoteViews? =
if (tasks.isHeader(position)) buildHeader(position) else buildUpdate(position)
override fun getLoadingView(): RemoteViews = newRemoteView()
override fun getViewTypeCount(): Int = 2
override fun getItemId(position: Int) = getTask(position)?.id ?: 0
override fun hasStableIds(): Boolean = true
private fun getCheckbox(task: Task): Bitmap = checkBoxProvider.getWidgetCheckBox(task)
private fun newRemoteView(): RemoteViews = RemoteViews(
BuildConfig.APPLICATION_ID,
if (isDark) R.layout.widget_row_dark else R.layout.widget_row_light
)
private fun buildHeader(position: Int): RemoteViews {
val row = RemoteViews(
BuildConfig.APPLICATION_ID,
if (isDark) R.layout.widget_header_dark else R.layout.widget_header_light
)
val section = tasks.getSection(position)
val sortGroup = section.value
val header: String? = if (filter?.supportsSorting() == true) {
headerFormatter.headerStringBlocking(
value = section.value,
groupMode = groupMode,
alwaysDisplayFullDate = showFullDate,
style = FormatStyle.MEDIUM,
compact = compact,
)
} else {
null
}
row.setTextViewText(R.id.header, header)
row.setImageViewResource(R.id.arrow, if (section.collapsed) {
R.drawable.ic_keyboard_arrow_down_black_18dp
} else {
R.drawable.ic_keyboard_arrow_up_black_18dp
})
row.setTextColor(
R.id.header,
section.headerColor(
context,
groupMode,
if (isDark) R.color.white_60 else R.color.black_60
)
)
if (!showDividers) {
row.setViewVisibility(R.id.divider, View.GONE)
}
row.setOnClickFillInIntent(
R.id.row,
Intent(WidgetClickActivity.TOGGLE_GROUP)
.putExtra(WidgetClickActivity.EXTRA_WIDGET, widgetId)
.putExtra(WidgetClickActivity.EXTRA_GROUP, sortGroup)
.putExtra(WidgetClickActivity.EXTRA_COLLAPSED, !section.collapsed)
)
return row
}
private fun buildUpdate(position: Int): RemoteViews? {
try {
val taskContainer = getTask(position) ?: return null
val task = taskContainer.task
var textColorTitle = textColorPrimary
val row = newRemoteView()
if (task.isHidden) {
textColorTitle = textColorSecondary
}
if (task.isCompleted) {
textColorTitle = textColorSecondary
row.setInt(
R.id.widget_text, "setPaintFlags", Paint.STRIKE_THRU_TEXT_FLAG or Paint.ANTI_ALIAS_FLAG)
} else {
row.setInt(R.id.widget_text, "setPaintFlags", Paint.ANTI_ALIAS_FLAG)
}
row.setFloat(R.id.widget_text, "setTextSize", textSize)
if (showDueDates) {
formatDueDate(row, taskContainer)
} else {
row.setViewVisibility(R.id.widget_due_bottom, View.GONE)
row.setViewVisibility(R.id.widget_due_end, View.GONE)
if (task.hasDueDate() && task.isOverdue) {
textColorTitle = context.getColor(R.color.overdue)
}
}
if (showFullTaskTitle) {
row.setInt(R.id.widget_text, "setMaxLines", Int.MAX_VALUE)
}
row.setTextViewText(
R.id.widget_text,
markdown.toMarkdown(task.title)
)
row.setTextColor(R.id.widget_text, textColorTitle)
if (showDescription && task.hasNotes()) {
row.setFloat(R.id.widget_description, "setTextSize", textSize)
row.setTextViewText(
R.id.widget_description,
markdown.toMarkdown(task.notes)
)
row.setViewVisibility(R.id.widget_description, View.VISIBLE)
if (showFullDescription) {
row.setInt(R.id.widget_description, "setMaxLines", Int.MAX_VALUE)
}
} else {
row.setViewVisibility(R.id.widget_description, View.GONE)
}
row.setOnClickFillInIntent(
R.id.widget_row,
Intent(WidgetClickActivity.EDIT_TASK)
.putExtra(WidgetClickActivity.EXTRA_FILTER, filter)
.putExtra(WidgetClickActivity.EXTRA_TASK, task))
if (showCheckboxes) {
row.setViewPadding(R.id.widget_complete_box, hPad, vPad, hPad, vPad)
row.setImageViewBitmap(R.id.widget_complete_box, getCheckbox(task))
row.setOnClickFillInIntent(
R.id.widget_complete_box,
Intent(WidgetClickActivity.COMPLETE_TASK)
.putExtra(WidgetClickActivity.EXTRA_TASK, task))
} else {
row.setViewPadding(R.id.widget_complete_box, hPad, 0, 0, 0)
row.setInt(R.id.widget_complete_box, "setBackgroundResource", 0)
}
row.setViewPadding(R.id.top_padding, 0, vPad, 0, 0)
row.setViewPadding(R.id.bottom_padding, 0, vPad, 0, 0)
if (!showDividers) {
row.setViewVisibility(R.id.divider, View.GONE)
}
row.removeAllViews(R.id.chips)
if (showSubtasks && taskContainer.hasChildren()) {
val chip = chipProvider.getSubtaskChip(taskContainer)
row.addView(R.id.chips, chip)
row.setOnClickFillInIntent(
R.id.chip,
Intent(WidgetClickActivity.TOGGLE_SUBTASKS)
.putExtra(WidgetClickActivity.EXTRA_TASK, task)
.putExtra(WidgetClickActivity.EXTRA_COLLAPSED, !taskContainer.isCollapsed)
)
}
if (taskContainer.isHidden && showStartDates) {
val sortByDate = groupMode == SortHelper.SORT_START && !disableGroups
chipProvider
.getStartDateChip(taskContainer, showFullDate, sortByDate)
?.let { row.addView(R.id.chips, it) }
}
if (taskContainer.hasLocation() && showPlaces) {
chipProvider
.getPlaceChip(filter, taskContainer)
?.let { row.addView(R.id.chips, it) }
}
if (!taskContainer.hasParent() && showLists) {
chipProvider
.getListChip(filter, taskContainer)
?.let { row.addView(R.id.chips, it) }
}
if (showTags && taskContainer.tagsString?.isNotBlank() == true) {
chipProvider
.getTagChips(filter, taskContainer)
.forEach { row.addView(R.id.chips, it) }
}
val startPad = taskContainer.indent * indentPadding
row.setViewPadding(R.id.widget_row, startPad, 0, 0, 0)
return row
} catch (e: Exception) {
Timber.e(e)
}
return null
}
private fun getTask(position: Int): TaskContainer? = tasks.getItem(position)
private suspend fun getQuery(filter: Filter?): List<String> {
val queries = getQuery(widgetPreferences, filter!!)
val last = queries.size - 1
queries[last] =
subtasksHelper.applySubtasksToWidgetFilter(filter, widgetPreferences, queries[last])
return queries
}
private fun formatDueDate(row: RemoteViews, task: TaskContainer) {
val dueDateRes = if (endDueDate) R.id.widget_due_end else R.id.widget_due_bottom
row.setViewVisibility(if (endDueDate) R.id.widget_due_bottom else R.id.widget_due_end, View.GONE)
val hasDueDate = task.hasDueDate()
val endPad = if (hasDueDate && endDueDate) 0 else hPad
row.setViewPadding(R.id.widget_text, 0, 0, endPad, 0)
if (hasDueDate) {
if (endDueDate) {
row.setViewPadding(R.id.widget_due_end, hPad, vPad, hPad, vPad)
}
row.setViewVisibility(dueDateRes, View.VISIBLE)
val text = if (
groupMode == SortHelper.SORT_DUE &&
(task.sortGroup ?: 0L) >= now().startOfDay() &&
!disableGroups
) {
task.takeIf { it.hasDueTime() }?.let {
DateUtilities.getTimeString(context, DateTimeUtils.newDateTime(task.dueDate))
}
} else {
DateUtilities.getRelativeDateTime(
context, task.dueDate, locale, FormatStyle.MEDIUM, showFullDate, false)
}
row.setTextViewText(dueDateRes, text)
row.setTextColor(
dueDateRes,
if (task.isOverdue) context.getColor(R.color.overdue) else textColorSecondary)
row.setFloat(dueDateRes, "setTextSize", dueDateTextSize)
if (handleDueDateClick) {
row.setOnClickFillInIntent(
dueDateRes,
Intent(WidgetClickActivity.RESCHEDULE_TASK)
.putExtra(WidgetClickActivity.EXTRA_TASK, task.task))
} else {
row.setInt(dueDateRes, "setBackgroundResource", 0)
}
} else {
row.setViewVisibility(dueDateRes, View.GONE)
}
}
private suspend fun updateSettings() {
vPad = widgetPreferences.widgetSpacing
hPad = context.resources.getDimension(R.dimen.widget_padding).toInt()
handleDueDateClick = widgetPreferences.rescheduleOnDueDateClick()
showFullTaskTitle = widgetPreferences.showFullTaskTitle()
showDescription = widgetPreferences.showDescription()
showFullDescription = widgetPreferences.showFullDescription()
chipProvider.isDark = isDark
textColorPrimary = context.getColor(if (isDark) R.color.white_87 else R.color.black_87)
textColorSecondary = context.getColor(if (isDark) R.color.white_60 else R.color.black_60)
val dueDatePosition = widgetPreferences.dueDatePosition
showDueDates = dueDatePosition != 2
endDueDate = dueDatePosition != 1
showCheckboxes = widgetPreferences.showCheckboxes()
textSize = widgetPreferences.fontSize.toFloat()
dueDateTextSize = max(10f, textSize - 2)
filter = defaultFilterProvider.getFilterFromPreference(widgetPreferences.filterId)
showDividers = widgetPreferences.showDividers()
disableGroups = filter?.let {
!it.supportsSorting()
|| (it.supportsManualSort() && widgetPreferences.isManualSort)
|| (it is AstridOrderingFilter && widgetPreferences.isAstridSort)
} == true
showPlaces = widgetPreferences.showPlaces()
showSubtasks = widgetPreferences.showSubtasks()
showStartDates = widgetPreferences.showStartDates()
showLists = widgetPreferences.showLists()
showTags = widgetPreferences.showTags()
showFullDate = widgetPreferences.alwaysDisplayFullDate
widgetPreferences.groupMode.takeIf { it != groupMode }
?.let {
if (groupMode != SortHelper.GROUP_NONE) {
widgetPreferences.collapsed = mutableSetOf(HEADER_COMPLETED)
}
groupMode = it
}
subtaskMode = widgetPreferences.subtaskMode
collapsed = widgetPreferences.collapsed
compact = widgetPreferences.compact
}
init {
val metrics = context.resources.displayMetrics
indentPadding = (20 * metrics.density).toInt()
}
}