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

402 lines
17 KiB
Kotlin

package org.tasks.widget
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Paint
import android.view.View
import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory
import androidx.annotation.StringRes
import com.todoroo.andlib.utility.DateUtilities
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.SubtaskInfo
import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.date.DateTimeUtils
import org.tasks.locale.Locale
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.SectionedDataSource
import org.tasks.time.DateTimeUtils.startOfDay
import org.tasks.ui.CheckBoxProvider
import timber.log.Timber
import java.time.format.FormatStyle
import java.util.*
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
) : 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 isRtl = false
private var collapsed = HashSet<Long>()
private var sortMode = -1
private var tasks = SectionedDataSource(emptyList(), false, 0, collapsed)
private val widgetPreferences = WidgetPreferences(context, preferences, widgetId)
private var isDark = checkIfDark
private var showFullDate = false
private val checkIfDark: Boolean
get() = when (widgetPreferences.themeIndex) {
0 -> false
3 -> context.isDark
else -> true
}
override fun onCreate() {}
override fun onDataSetChanged() {
runBlocking {
updateSettings()
tasks = SectionedDataSource(
taskDao.fetchTasks { getQuery(filter, it) },
disableGroups,
sortMode,
collapsed
)
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) {
getHeader(sortMode, section.value)
} 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
})
val color = if (sortMode == SortHelper.SORT_DUE
&& sortGroup > 0
&& DateTimeUtils.newDateTime(sortGroup).plusDays(1).startOfDay().isBeforeNow) {
context.getColor(R.color.overdue)
} else if (sortMode == SortHelper.SORT_START
&& sortGroup > 0
&& DateTimeUtils.newDateTime(sortGroup).plusDays(1).startOfDay().isBeforeNow) {
context.getColor(R.color.overdue)
} else {
textColorSecondary
}
row.setTextColor(R.id.header, color)
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 getHeader(sortMode: Int, group: Long): String = when {
sortMode == SortHelper.SORT_IMPORTANCE -> context.getString(priorityToString(group.toInt()))
group == 0L -> context.getString(when (sortMode) {
SortHelper.SORT_DUE -> R.string.no_due_date
SortHelper.SORT_START -> R.string.no_start_date
else -> R.string.no_date
})
sortMode == SortHelper.SORT_CREATED ->
context.getString(R.string.sort_created_group, getDateString(group))
sortMode == SortHelper.SORT_MODIFIED ->
context.getString(R.string.sort_modified_group, getDateString(group))
else -> getDateString(group, false)
}
private fun getDateString(value: Long, lowercase: Boolean = true) =
DateUtilities.getRelativeDay(context, value, locale.locale, FormatStyle.MEDIUM, showFullDate, lowercase)
@StringRes
private fun priorityToString(priority: Int) = when (priority) {
0 -> R.string.filter_high_priority
1 -> R.string.filter_medium_priority
2 -> R.string.filter_low_priority
else -> R.string.filter_no_priority
}
private fun buildUpdate(position: Int): RemoteViews? {
try {
val taskContainer = getTask(position) ?: return null
val task = taskContainer.getTask()
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, 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, 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 = sortMode == 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.tags?.isNotBlank() == true) {
chipProvider
.getTagChips(filter, taskContainer)
.forEach { row.addView(R.id.chips, it) }
}
row.setInt(R.id.widget_row, "setLayoutDirection", locale.directionality)
val startPad = taskContainer.getIndent() * indentPadding
row.setViewPadding(R.id.widget_row, if (isRtl) 0 else startPad, 0, if (isRtl) startPad else 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?, subtasks: SubtaskInfo): List<String> {
val queries = getQuery(widgetPreferences, filter!!, subtasks)
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, if (isRtl) endPad else 0, 0, if (isRtl) 0 else 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 (sortMode == SortHelper.SORT_DUE
&& !disableGroups
&& task.sortGroup?.startOfDay() == task.dueDate.startOfDay()) {
task.takeIf { it.hasDueTime() }?.let {
DateUtilities.getTimeString(context, DateTimeUtils.newDateTime(task.dueDate))
}
} else {
DateUtilities.getRelativeDateTime(
context, task.dueDate, locale.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 = widgetPreferences.disableGroups() || filter?.let {
!it.supportsSorting()
|| (it.supportsManualSort() && widgetPreferences.isManualSort)
|| (it.supportsAstridSorting() && widgetPreferences.isAstridSort)
} == true
showPlaces = widgetPreferences.showPlaces()
showSubtasks = widgetPreferences.showSubtasks()
showStartDates = widgetPreferences.showStartDates()
showLists = widgetPreferences.showLists()
showTags = widgetPreferences.showTags()
showFullDate = widgetPreferences.alwaysDisplayFullDate
widgetPreferences.sortMode.takeIf { it != sortMode }
?.let {
if (sortMode >= 0) {
widgetPreferences.collapsed = HashSet()
}
sortMode = it
}
collapsed = widgetPreferences.collapsed
isRtl = locale.directionality == View.LAYOUT_DIRECTION_RTL
}
init {
val metrics = context.resources.displayMetrics
indentPadding = (20 * metrics.density).toInt()
}
companion object {
val Context.isDark: Boolean
get() = (Configuration.UI_MODE_NIGHT_YES ==
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK))
}
}