mirror of https://github.com/tasks/tasks
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.
402 lines
17 KiB
Kotlin
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))
|
|
}
|
|
} |