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() 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 { 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)) } }