Java tutorial
/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.adapter; import android.app.PendingIntent.CanceledException; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Paint; import android.support.v7.app.AlertDialog; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.TextView; import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property.IntegerProperty; import com.todoroo.andlib.data.Property.LongProperty; import com.todoroo.andlib.data.Property.StringProperty; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Functions; import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.Pair; import com.todoroo.astrid.activity.TaskListFragment; import com.todoroo.astrid.api.PermaSql; import com.todoroo.astrid.api.TaskAction; import com.todoroo.astrid.core.LinkActionExposer; import com.todoroo.astrid.dao.TaskAttachmentDao; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.data.RemoteModel; import com.todoroo.astrid.data.TagData; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.TaskAttachment; import com.todoroo.astrid.files.FilesAction; import com.todoroo.astrid.notes.NotesAction; import com.todoroo.astrid.tags.TagService; import com.todoroo.astrid.tags.TaskToTagMetadata; import com.todoroo.astrid.ui.CheckableImageView; import org.tasks.R; import org.tasks.dialogs.DialogBuilder; import org.tasks.preferences.Preferences; import org.tasks.themes.ThemeCache; import org.tasks.themes.ThemeColor; import org.tasks.ui.CheckBoxes; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import timber.log.Timber; import static android.support.v4.content.ContextCompat.getColor; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.transform; import static org.tasks.preferences.ResourceResolver.getData; /** * Adapter for displaying a user's tasks as a list * * @author Tim Su <tim@todoroo.com> * */ public class TaskAdapter extends CursorAdapter implements Filterable { public interface OnCompletedTaskListener { void onCompletedTask(Task item, boolean newState); } private static final char SPACE = '\u0020'; private static final char HAIR_SPACE = '\u200a'; private static final StringProperty TAGS = new StringProperty(null, "group_concat(nullif(" + TaskListFragment.TAGS_METADATA_JOIN + "." + TaskToTagMetadata.TAG_UUID.name + ", '')" + ", ',')") .as("tags"); private static final LongProperty FILE_ID_PROPERTY = TaskAttachment.ID .cloneAs(TaskListFragment.FILE_METADATA_JOIN, "fileId"); private static final IntegerProperty HAS_NOTES_PROPERTY = new IntegerProperty(null, "length(" + Task.NOTES + ") > 0").as("hasNotes"); // --- other constants /** Properties that need to be read from the action item */ public static final Property<?>[] PROPERTIES = new Property<?>[] { Task.ID, Task.UUID, Task.TITLE, Task.IMPORTANCE, Task.DUE_DATE, Task.COMPLETION_DATE, Task.MODIFICATION_DATE, Task.HIDE_UNTIL, Task.DELETION_DATE, Task.ELAPSED_SECONDS, Task.TIMER_START, Task.RECURRENCE, Task.REMINDER_LAST, HAS_NOTES_PROPERTY, // Whether or not the task has notes TAGS, // Concatenated list of tags FILE_ID_PROPERTY // File id }; // --- instance variables private final CheckBoxes checkBoxes; private final Preferences preferences; private final TaskAttachmentDao taskAttachmentDao; private final TaskDao taskDao; private final Context context; private final TaskListFragment fragment; private final DialogBuilder dialogBuilder; private final TagService tagService; private final ThemeCache themeCache; private final Resources resources; private OnCompletedTaskListener onCompletedTaskListener = null; private final LayoutInflater inflater; private int fontSize; private final AtomicReference<String> query; // measure utilities private final DisplayMetrics displayMetrics; private final int minRowHeight; private final float tagCharacters; private final Map<String, TagData> tagMap = new HashMap<>(); private final int textColorSecondary; private final int textColorHint; private final int textColorOverdue; public TaskAdapter(Context context, Preferences preferences, TaskAttachmentDao taskAttachmentDao, TaskDao taskDao, TaskListFragment fragment, Cursor c, AtomicReference<String> query, DialogBuilder dialogBuilder, CheckBoxes checkBoxes, TagService tagService, ThemeCache themeCache) { super(context, c, false); this.checkBoxes = checkBoxes; this.preferences = preferences; this.taskAttachmentDao = taskAttachmentDao; this.taskDao = taskDao; this.context = context; this.query = query; this.fragment = fragment; this.dialogBuilder = dialogBuilder; this.tagService = tagService; this.themeCache = themeCache; this.resources = fragment.getResources(); inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); TypedValue typedValue = new TypedValue(); context.getResources().getValue(R.dimen.tag_characters, typedValue, true); tagCharacters = typedValue.getFloat(); fontSize = preferences.getIntegerFromString(R.string.p_fontSize, 18); displayMetrics = new DisplayMetrics(); fragment.getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); textColorSecondary = getData(context, android.R.attr.textColorSecondary); textColorHint = getData(context, android.R.attr.textColorTertiary); textColorOverdue = getColor(context, R.color.overdue); updateTagMap(); this.minRowHeight = computeMinRowHeight(); } private int computeMinRowHeight() { DisplayMetrics metrics = resources.getDisplayMetrics(); return (int) (metrics.density * 40); } public int computeFullRowHeight() { DisplayMetrics metrics = resources.getDisplayMetrics(); if (fontSize < 16) { return (int) (39 * metrics.density); } else { return minRowHeight + (int) (10 * metrics.density); } } /* ====================================================================== * =========================================================== filterable * ====================================================================== */ @Override public Cursor runQueryOnBackgroundThread(CharSequence constraint) { if (getFilterQueryProvider() != null) { return getFilterQueryProvider().runQuery(constraint); } return fetchFiltered(query.get(), constraint, fragment.taskProperties()); } /** * Fetch tasks for the given filter * @param constraint text constraint, or null */ private TodorooCursor<Task> fetchFiltered(String queryTemplate, CharSequence constraint, Property<?>... properties) { Criterion whereConstraint = null; if (constraint != null) { whereConstraint = Functions.upper(Task.TITLE).like("%" + constraint.toString().toUpperCase() + "%"); } if (queryTemplate == null) { if (whereConstraint == null) { return taskDao.query(Query.selectDistinct(properties)); } else { return taskDao.query(Query.selectDistinct(properties).where(whereConstraint)); } } String sql; if (whereConstraint != null) { if (!queryTemplate.toUpperCase().contains("WHERE")) { sql = queryTemplate + " WHERE " + whereConstraint; } else { sql = queryTemplate.replace("WHERE ", "WHERE " + whereConstraint + " AND "); } } else { sql = queryTemplate; } sql = PermaSql.replacePlaceholders(sql); return taskDao.query(Query.select(properties).withQueryTemplate(sql)); } /* ====================================================================== * =========================================================== view setup * ====================================================================== */ /** Creates a new view for use in the list view */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { ViewGroup view = (ViewGroup) inflater.inflate(R.layout.task_adapter_row_simple, parent, false); // create view holder ViewHolder viewHolder = new ViewHolder(); viewHolder.task = new Task(); viewHolder.rowBody = (ViewGroup) view.findViewById(R.id.rowBody); viewHolder.nameView = (TextView) view.findViewById(R.id.title); viewHolder.completeBox = (CheckableImageView) view.findViewById(R.id.completeBox); viewHolder.dueDate = (TextView) view.findViewById(R.id.due_date); viewHolder.tagBlock = (TextView) view.findViewById(R.id.tag_block); viewHolder.taskActionContainer = view.findViewById(R.id.taskActionContainer); viewHolder.taskActionIcon = (ImageView) view.findViewById(R.id.taskActionIcon); boolean showFullTaskTitle = preferences.getBoolean(R.string.p_fullTaskTitle, false); if (showFullTaskTitle) { viewHolder.nameView.setMaxLines(Integer.MAX_VALUE); viewHolder.nameView.setSingleLine(false); viewHolder.nameView.setEllipsize(null); } view.setTag(viewHolder); for (int i = 0; i < view.getChildCount(); i++) { view.getChildAt(i).setTag(viewHolder); } // add UI component listeners addListeners(view); return view; } /** Populates a view with content */ @Override public void bindView(View view, Context context, Cursor c) { TodorooCursor<Task> cursor = (TodorooCursor<Task>) c; ViewHolder viewHolder = ((ViewHolder) view.getTag()); viewHolder.tagsString = cursor.get(TAGS); viewHolder.hasFiles = cursor.get(FILE_ID_PROPERTY) > 0; viewHolder.hasNotes = cursor.get(HAS_NOTES_PROPERTY) > 0; // TODO: see if this is a performance issue viewHolder.task = new Task(cursor); setFieldContentsAndVisibility(view); setTaskAppearance(viewHolder, viewHolder.task); } public String getItemUuid(int position) { TodorooCursor<Task> c = (TodorooCursor<Task>) getCursor(); if (c != null) { if (c.moveToPosition(position)) { return c.get(Task.UUID); } else { return RemoteModel.NO_UUID; } } else { return RemoteModel.NO_UUID; } } /** * View Holder saves a lot of findViewById lookups. * * @author Tim Su <tim@todoroo.com> * */ public static class ViewHolder { public Task task; public ViewGroup rowBody; public TextView nameView; public CheckableImageView completeBox; public TextView dueDate; public TextView tagBlock; public View taskActionContainer; public ImageView taskActionIcon; public String tagsString; // From join query, not part of the task model public boolean hasFiles; // From join query, not part of the task model public boolean hasNotes; } /** Helper method to set the contents and visibility of each field */ public synchronized void setFieldContentsAndVisibility(View view) { ViewHolder viewHolder = (ViewHolder) view.getTag(); Task task = viewHolder.task; if (fontSize < 16) { viewHolder.rowBody.setMinimumHeight(0); viewHolder.completeBox.setMinimumHeight(0); } else { viewHolder.rowBody.setMinimumHeight(minRowHeight); viewHolder.completeBox.setMinimumHeight(minRowHeight); } // name final TextView nameView = viewHolder.nameView; { String nameValue = task.getTitle(); long hiddenUntil = task.getHideUntil(); if (task.getDeletionDate() > 0) { nameValue = resources.getString(R.string.TAd_deletedFormat, nameValue); } if (hiddenUntil > DateUtilities.now()) { nameValue = resources.getString(R.string.TAd_hiddenFormat, nameValue); } nameView.setText(nameValue); } setupDueDateAndTags(viewHolder, task); // Task action ImageView taskAction = viewHolder.taskActionIcon; if (taskAction != null) { TaskAction action = getTaskAction(task, viewHolder.hasFiles, viewHolder.hasNotes); if (action != null) { viewHolder.taskActionContainer.setVisibility(View.VISIBLE); taskAction.setImageResource(action.icon); taskAction.setTag(action); } else { viewHolder.taskActionContainer.setVisibility(View.GONE); taskAction.setTag(null); } } } private TaskAction getTaskAction(Task task, boolean hasFiles, boolean hasNotes) { if (task.isCompleted()) { return null; } return LinkActionExposer.getActionsForTask(context, task, hasFiles, hasNotes); } public void onClick(View v) { // expand view (unless deleted) final ViewHolder viewHolder = (ViewHolder) v.getTag(); if (viewHolder.task.isDeleted()) { return; } long taskId = viewHolder.task.getId(); fragment.onTaskListItemClicked(taskId); } private Pair<Float, Float> lastTouchYRawY = new Pair<>(0f, 0f); /** * Set listeners for this view. This is called once per view when it is * created. */ private void addListeners(final View container) { final ViewHolder viewHolder = (ViewHolder) container.getTag(); // check box listener OnTouchListener otl = (v, event) -> { lastTouchYRawY = new Pair<>(event.getY(), event.getRawY()); return false; }; viewHolder.completeBox.setOnTouchListener(otl); viewHolder.completeBox.setOnClickListener(completeBoxListener); if (viewHolder.taskActionContainer != null) { viewHolder.taskActionContainer.setOnClickListener(v -> { TaskAction action = (TaskAction) viewHolder.taskActionIcon.getTag(); if (action instanceof NotesAction) { showEditNotesDialog(viewHolder.task); } else if (action instanceof FilesAction) { showFilesDialog(viewHolder.task); } else if (action != null) { try { action.intent.send(); } catch (CanceledException e) { // Oh well Timber.e(e, e.getMessage()); } } }); } } private void showEditNotesDialog(final Task task) { Task t = taskDao.fetch(task.getId(), Task.NOTES); if (t == null || !t.hasNotes()) { return; } SpannableString description = new SpannableString(t.getNotes()); Linkify.addLinks(description, Linkify.ALL); AlertDialog dialog = dialogBuilder.newDialog().setMessage(description) .setPositiveButton(android.R.string.ok, null).show(); View message = dialog.findViewById(android.R.id.message); if (message != null && message instanceof TextView) { ((TextView) message).setMovementMethod(LinkMovementMethod.getInstance()); } } private void showFilesDialog(Task task) { // TODO: reimplement this // FilesControlSet filesControlSet = new FilesControlSet(); // filesControlSet.hideAddAttachmentButton(); // filesControlSet.readFromTask(task); // filesControlSet.getView().performClick(); } /* ====================================================================== * ======================================================= event handlers * ====================================================================== */ @Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); fontSize = preferences.getIntegerFromString(R.string.p_fontSize, 18); updateTagMap(); } private void updateTagMap() { tagMap.clear(); for (TagData tagData : tagService.getTagList()) { tagMap.put(tagData.getUuid(), tagData); } } private final View.OnClickListener completeBoxListener = v -> { int[] location = new int[2]; v.getLocationOnScreen(location); ViewHolder viewHolder = getTagFromCheckBox(v); if (Math.abs(location[1] + lastTouchYRawY.getLeft() - lastTouchYRawY.getRight()) > 10) { viewHolder.completeBox.setChecked(!viewHolder.completeBox.isChecked()); return; } Task task = viewHolder.task; completeTask(task, viewHolder.completeBox.isChecked()); // set check box to actual action item state setTaskAppearance(viewHolder, task); }; private ViewHolder getTagFromCheckBox(View v) { return (ViewHolder) ((View) v.getParent()).getTag(); } /** Helper method to adjust a tasks' appearance if the task is completed or * uncompleted. */ private void setTaskAppearance(ViewHolder viewHolder, Task task) { boolean completed = task.isCompleted(); TextView name = viewHolder.nameView; if (completed) { name.setEnabled(false); name.setPaintFlags(name.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); } else { name.setEnabled(true); name.setPaintFlags(name.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); } name.setTextSize(fontSize); setupDueDateAndTags(viewHolder, task); float detailTextSize = Math.max(10, fontSize * 14 / 20); if (viewHolder.dueDate != null) { viewHolder.dueDate.setTextSize(detailTextSize); viewHolder.dueDate.setTypeface(null, 0); } setupCompleteBox(viewHolder); } private void setupCompleteBox(ViewHolder viewHolder) { // complete box final Task task = viewHolder.task; final CheckableImageView checkBoxView = viewHolder.completeBox; boolean completed = task.isCompleted(); checkBoxView.setChecked(completed); if (completed) { checkBoxView.setImageDrawable(checkBoxes.getCompletedCheckbox(task.getImportance())); } else if (TextUtils.isEmpty(task.getRecurrence())) { checkBoxView.setImageDrawable(checkBoxes.getCheckBox(task.getImportance())); } else { checkBoxView.setImageDrawable(checkBoxes.getRepeatingCheckBox(task.getImportance())); } checkBoxView.invalidate(); } private final Function<String, TagData> uuidToTag = tagMap::get; private final Ordering<TagData> orderByName = new Ordering<TagData>() { @Override public int compare(TagData left, TagData right) { return left.getName().compareTo(right.getName()); } }; private final Ordering<TagData> orderByLength = new Ordering<TagData>() { @Override public int compare(TagData left, TagData right) { int leftLength = left.getName().length(); int rightLength = right.getName().length(); if (leftLength < rightLength) { return -1; } else if (rightLength < leftLength) { return 1; } else { return 0; } } }; private Function<TagData, SpannableString> tagToString(final float maxLength) { return tagData -> { String tagName = tagData.getName(); tagName = tagName.substring(0, Math.min(tagName.length(), (int) maxLength)); SpannableString string = new SpannableString(SPACE + tagName + SPACE); int themeIndex = tagData.getColor(); ThemeColor color = themeIndex >= 0 ? themeCache.getThemeColor(themeIndex) : themeCache.getUntaggedColor(); string.setSpan(new BackgroundColorSpan(color.getPrimaryColor()), 0, string.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); string.setSpan(new ForegroundColorSpan(color.getActionBarTint()), 0, string.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return string; }; } // Returns due date text width private void setupDueDateAndTags(ViewHolder viewHolder, Task task) { // due date / completion date final TextView dueDateView = viewHolder.dueDate; { if (!task.isCompleted() && task.hasDueDate()) { long dueDate = task.getDueDate(); if (task.isOverdue()) { dueDateView.setTextColor(textColorOverdue); } else { dueDateView.setTextColor(textColorSecondary); } String dateValue = DateUtilities.getRelativeDateStringWithTime(context, dueDate); dueDateView.setText(dateValue); dueDateView.setVisibility(View.VISIBLE); } else if (task.isCompleted()) { String dateValue = DateUtilities.getRelativeDateStringWithTime(context, task.getCompletionDate()); dueDateView.setText(resources.getString(R.string.TAd_completed, dateValue)); dueDateView.setTextColor(textColorHint); dueDateView.setVisibility(View.VISIBLE); } else { dueDateView.setVisibility(View.GONE); } if (task.isCompleted()) { viewHolder.tagBlock.setVisibility(View.GONE); } else { String tags = viewHolder.tagsString; List<String> tagUuids = tags != null ? newArrayList(tags.split(",")) : Lists.newArrayList(); Iterable<TagData> t = filter(transform(tagUuids, uuidToTag), Predicates.notNull()); List<TagData> firstFourByName = orderByName.leastOf(t, 4); int numTags = firstFourByName.size(); if (numTags > 0) { List<TagData> firstFourByNameLength = orderByLength.sortedCopy(firstFourByName); float maxLength = tagCharacters / numTags; for (int i = 0; i < numTags - 1; i++) { TagData tagData = firstFourByNameLength.get(i); String name = tagData.getName(); if (name.length() >= maxLength) { break; } float excess = maxLength - name.length(); int beneficiaries = numTags - i - 1; float additional = excess / beneficiaries; maxLength += additional; } List<SpannableString> tagStrings = transform(firstFourByName, tagToString(maxLength)); SpannableStringBuilder builder = new SpannableStringBuilder(); for (SpannableString tagString : tagStrings) { if (builder.length() > 0) { builder.append(HAIR_SPACE); } builder.append(tagString); } viewHolder.tagBlock.setText(builder); viewHolder.tagBlock.setVisibility(View.VISIBLE); } else { viewHolder.tagBlock.setVisibility(View.GONE); } } } } /** * This method is called when user completes a task via check box or other * means * * @param newState * state that this task should be set to */ private void completeTask(final Task task, final boolean newState) { if (task == null) { return; } if (newState != task.isCompleted()) { if (onCompletedTaskListener != null) { onCompletedTaskListener.onCompletedTask(task, newState); } taskDao.setComplete(task, newState); } } /** * Add a new listener */ public void addOnCompletedTaskListener(final OnCompletedTaskListener newListener) { if (this.onCompletedTaskListener == null) { this.onCompletedTaskListener = newListener; } else { final OnCompletedTaskListener old = this.onCompletedTaskListener; this.onCompletedTaskListener = (item, newState) -> { old.onCompletedTask(item, newState); newListener.onCompletedTask(item, newState); }; } } }