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/astrid/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java

1322 lines
51 KiB
Java

/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.adapter;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.app.Dialog;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.text.Html;
import android.text.Html.ImageGetter;
import android.text.Html.TagHandler;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.Animation;
import android.view.animation.ScaleAnimation;
import android.widget.CheckBox;
import android.widget.CursorAdapter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.tasks.R;
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.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Pair;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.TaskAction;
import com.todoroo.astrid.api.TaskDecoration;
import com.todoroo.astrid.api.TaskDecorationExposer;
import com.todoroo.astrid.core.LinkActionExposer;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskAttachment;
import com.todoroo.astrid.data.User;
import com.todoroo.astrid.files.FilesAction;
import com.todoroo.astrid.files.FilesControlSet;
import com.todoroo.astrid.helper.AsyncImageView;
import com.todoroo.astrid.helper.TaskAdapterAddOnManager;
import com.todoroo.astrid.notes.NotesAction;
import com.todoroo.astrid.notes.NotesDecorationExposer;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.service.ThemeService;
import com.todoroo.astrid.tags.TaskToTagMetadata;
import com.todoroo.astrid.timers.TimerDecorationExposer;
import com.todoroo.astrid.ui.CheckableImageView;
import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.ResourceDrawableCache;
/**
* 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 {
public void onCompletedTask(Task item, boolean newState);
}
public static final String DETAIL_SEPARATOR = " | "; //$NON-NLS-1$
public static final String BROADCAST_EXTRA_TASK = "model"; //$NON-NLS-1$
private static final StringProperty TAGS = new StringProperty(null, "group_concat(nullif(" + TaskListFragment.TAGS_METADATA_JOIN + "." + TaskToTagMetadata.TAG_NAME.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");
private static final StringProperty PICTURE = User.PICTURE.cloneAs(TaskListFragment.USER_IMAGE_JOIN, null);
// --- 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.IS_READONLY,
Task.IS_PUBLIC,
Task.IMPORTANCE,
Task.DUE_DATE,
Task.COMPLETION_DATE,
Task.MODIFICATION_DATE,
Task.HIDE_UNTIL,
Task.DELETION_DATE,
Task.DETAILS,
Task.ELAPSED_SECONDS,
Task.TIMER_START,
Task.RECURRENCE,
Task.USER_ID,
Task.USER,
Task.REMINDER_LAST,
Task.SOCIAL_REMINDER,
PICTURE,
HAS_NOTES_PROPERTY, // Whether or not the task has notes
TAGS, // Concatenated list of tags
FILE_ID_PROPERTY // File id
};
public static final Property<?>[] BASIC_PROPERTIES = new Property<?>[] {
Task.ID,
Task.UUID,
Task.TITLE,
Task.IS_READONLY,
Task.IS_PUBLIC,
Task.IMPORTANCE,
Task.RECURRENCE,
Task.COMPLETION_DATE,
Task.HIDE_UNTIL,
Task.DELETION_DATE
};
public static final int[] IMPORTANCE_RESOURCES = new int[] {
R.drawable.check_box_1,
R.drawable.check_box_2,
R.drawable.check_box_3,
R.drawable.check_box_4,
};
public static final int[] IMPORTANCE_RESOURCES_CHECKED = new int[] {
R.drawable.check_box_checked_1,
R.drawable.check_box_checked_2,
R.drawable.check_box_checked_3,
R.drawable.check_box_checked_4,
};
public static final int[] IMPORTANCE_RESOURCES_LARGE = new int[] {
R.drawable.check_box_large_1,
R.drawable.check_box_large_2,
R.drawable.check_box_large_3,
R.drawable.check_box_large_4,
};
public static final int[] IMPORTANCE_REPEAT_RESOURCES = new int[] {
R.drawable.check_box_repeat_1,
R.drawable.check_box_repeat_2,
R.drawable.check_box_repeat_3,
R.drawable.check_box_repeat_4,
};
public static final int[] IMPORTANCE_REPEAT_RESOURCES_CHECKED = new int[] {
R.drawable.check_box_repeat_checked_1,
R.drawable.check_box_repeat_checked_2,
R.drawable.check_box_repeat_checked_3,
R.drawable.check_box_repeat_checked_4,
};
private static final Drawable[] IMPORTANCE_DRAWABLES = new Drawable[IMPORTANCE_RESOURCES.length];
private static final Drawable[] IMPORTANCE_DRAWABLES_CHECKED = new Drawable[IMPORTANCE_RESOURCES_CHECKED.length];
private static final Drawable[] IMPORTANCE_DRAWABLES_LARGE = new Drawable[IMPORTANCE_RESOURCES_LARGE.length];
private static final Drawable[] IMPORTANCE_REPEAT_DRAWABLES = new Drawable[IMPORTANCE_REPEAT_RESOURCES.length];
private static final Drawable[] IMPORTANCE_REPEAT_DRAWABLES_CHECKED = new Drawable[IMPORTANCE_REPEAT_RESOURCES_CHECKED.length];
// --- instance variables
@Autowired
protected TaskService taskService;
public static int APPLY_LISTENERS_PARENT = 0;
public static int APPLY_LISTENERS_ROW_BODY= 1;
public static int APPLY_LISTENERS_NONE = 2;
protected final Context context;
protected final TaskListFragment fragment;
protected final Resources resources;
protected final HashMap<Object, Boolean> completedItems = new HashMap<Object, Boolean>(0);
protected OnCompletedTaskListener onCompletedTaskListener = null;
protected final int resource;
protected final LayoutInflater inflater;
private DetailLoaderThread detailLoader;
private int fontSize;
protected int applyListeners = APPLY_LISTENERS_PARENT;
private long mostRecentlyMade = -1;
private final ScaleAnimation scaleAnimation;
private final int readonlyBackground;
private final AtomicReference<String> query;
// measure utilities
protected final Paint paint;
protected final DisplayMetrics displayMetrics;
private final boolean simpleLayout;
private final boolean titleOnlyLayout;
protected final int minRowHeight;
// --- task detail and decoration soft caches
public final DecorationManager decorationManager;
private final Map<Long, TaskAction> taskActionLoader = Collections.synchronizedMap(new HashMap<Long, TaskAction>());
/**
* Constructor
*
* @param fragment
* @param resource
* layout resource to inflate
* @param c
* database cursor
* @param autoRequery
* whether cursor is automatically re-queried on changes
* @param onCompletedTaskListener
* task listener. can be null
*/
public TaskAdapter(TaskListFragment fragment, int resource,
Cursor c, AtomicReference<String> query, boolean autoRequery,
OnCompletedTaskListener onCompletedTaskListener) {
super(ContextManager.getContext(), c, autoRequery);
DependencyInjectionService.getInstance().inject(this);
this.context = ContextManager.getContext();
this.query = query;
this.resource = resource;
this.titleOnlyLayout = resource == R.layout.task_adapter_row_title_only;
this.fragment = fragment;
this.resources = fragment.getResources();
this.onCompletedTaskListener = onCompletedTaskListener;
inflater = (LayoutInflater) fragment.getActivity().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
fontSize = Preferences.getIntegerFromString(R.string.p_fontSize, 18);
paint = new Paint();
displayMetrics = new DisplayMetrics();
fragment.getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
this.simpleLayout = (resource == R.layout.task_adapter_row_simple);
this.minRowHeight = computeMinRowHeight();
startDetailThread();
decorationManager = new DecorationManager();
scaleAnimation = new ScaleAnimation(1.4f, 1.0f, 1.4f, 1.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setDuration(100);
TypedValue readonlyBg = new TypedValue();
fragment.getActivity().getTheme().resolveAttribute(R.attr.asReadonlyTaskBackground, readonlyBg, false);
readonlyBackground = readonlyBg.data;
preloadDrawables(IMPORTANCE_RESOURCES, IMPORTANCE_DRAWABLES);
preloadDrawables(IMPORTANCE_RESOURCES_CHECKED, IMPORTANCE_DRAWABLES_CHECKED);
preloadDrawables(IMPORTANCE_RESOURCES_LARGE, IMPORTANCE_DRAWABLES_LARGE);
preloadDrawables(IMPORTANCE_REPEAT_RESOURCES, IMPORTANCE_REPEAT_DRAWABLES);
preloadDrawables(IMPORTANCE_REPEAT_RESOURCES_CHECKED, IMPORTANCE_REPEAT_DRAWABLES_CHECKED);
}
private void preloadDrawables(int[] resourceIds, Drawable[] drawables) {
for (int i = 0; i < resourceIds.length; i++) {
drawables[i] = resources.getDrawable(resourceIds[i]);
}
}
protected int computeMinRowHeight() {
DisplayMetrics metrics = resources.getDisplayMetrics();
if (simpleLayout || titleOnlyLayout) {
return (int) (metrics.density * 40);
} else {
return (int) (metrics.density * 45);
}
}
public int computeFullRowHeight() {
DisplayMetrics metrics = resources.getDisplayMetrics();
if (fontSize < 16) {
return (int) (39 * metrics.density);
} else {
return minRowHeight + (int) (10 * metrics.density);
}
}
private void startDetailThread() {
if (Preferences.getBoolean(R.string.p_showNotes, false) && !simpleLayout && !titleOnlyLayout) {
detailLoader = new DetailLoaderThread();
detailLoader.start();
}
}
/* ======================================================================
* =========================================================== filterable
* ====================================================================== */
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
if (getFilterQueryProvider() != null) {
return getFilterQueryProvider().runQuery(constraint);
}
// perform query
TodorooCursor<Task> newCursor = taskService.fetchFiltered(
query.get(), constraint, fragment.taskProperties());
return newCursor;
}
public String getQuery() {
return query.get();
}
/* ======================================================================
* =========================================================== 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(resource, parent, false);
// create view holder
ViewHolder viewHolder = new ViewHolder();
viewHolder.task = new Task();
viewHolder.view = view;
viewHolder.rowBody = (ViewGroup)view.findViewById(R.id.rowBody);
viewHolder.nameView = (TextView)view.findViewById(R.id.title);
viewHolder.picture = (AsyncImageView)view.findViewById(R.id.picture);
viewHolder.pictureBorder = (ImageView)view.findViewById(R.id.pictureBorder);
viewHolder.completeBox = (CheckableImageView)view.findViewById(R.id.completeBox);
viewHolder.dueDate = (TextView)view.findViewById(R.id.dueDate);
viewHolder.tagsView = (TextView)view.findViewById(R.id.tagsDisplay);
viewHolder.details1 = (TextView)view.findViewById(R.id.details1);
viewHolder.details2 = (TextView)view.findViewById(R.id.details2);
viewHolder.taskRow = (LinearLayout)view.findViewById(R.id.task_row);
viewHolder.taskActionContainer = view.findViewById(R.id.taskActionContainer);
viewHolder.taskActionIcon = (ImageView)view.findViewById(R.id.taskActionIcon);
boolean showFullTaskTitle = Preferences.getBoolean(R.string.p_fullTaskTitle, false);
boolean showNotes = Preferences.getBoolean(R.string.p_showNotes, false);
if (showFullTaskTitle && !titleOnlyLayout) {
viewHolder.nameView.setMaxLines(Integer.MAX_VALUE);
viewHolder.nameView.setSingleLine(false);
viewHolder.nameView.setEllipsize(null);
} else if (titleOnlyLayout) {
viewHolder.nameView.setMaxLines(1);
viewHolder.nameView.setSingleLine(true);
viewHolder.nameView.setEllipsize(TruncateAt.END);
}
if (showNotes && !simpleLayout && !titleOnlyLayout) {
RelativeLayout.LayoutParams taskRowParams = (RelativeLayout.LayoutParams)viewHolder.taskRow.getLayoutParams();
taskRowParams.addRule(RelativeLayout.CENTER_VERTICAL, 0);
}
view.setTag(viewHolder);
for(int i = 0; i < view.getChildCount(); i++) {
view.getChildAt(i).setTag(viewHolder);
}
if(viewHolder.details1 != null) {
viewHolder.details1.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());
if (!titleOnlyLayout) {
viewHolder.tagsString = cursor.get(TAGS);
viewHolder.imageUrl = RemoteModel.PictureHelper.getPictureUrlFromCursor(cursor, PICTURE, RemoteModel.PICTURE_THUMB);
viewHolder.hasFiles = cursor.get(FILE_ID_PROPERTY) > 0;
viewHolder.hasNotes = cursor.get(HAS_NOTES_PROPERTY) > 0;
}
Task task = viewHolder.task;
task.clear();
task.readFromCursor(cursor);
setFieldContentsAndVisibility(view);
setTaskAppearance(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 view;
public ViewGroup rowBody;
public TextView nameView;
public CheckableImageView completeBox;
public AsyncImageView picture;
public ImageView pictureBorder;
public TextView dueDate;
public TextView tagsView;
public TextView details1, details2;
public LinearLayout taskRow;
public View taskActionContainer;
public ImageView taskActionIcon;
public String tagsString; // From join query, not part of the task model
public String imageUrl; // From join query, not part of the task model
public boolean hasFiles; // From join query, not part of the task model
public boolean hasNotes;
public View[] decorations;
}
/** 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 || titleOnlyLayout) {
viewHolder.rowBody.setMinimumHeight(0);
viewHolder.completeBox.setMinimumHeight(0);
} else {
viewHolder.rowBody.setMinimumHeight(minRowHeight);
viewHolder.completeBox.setMinimumHeight(minRowHeight);
}
if (task.isEditable()) {
viewHolder.view.setBackgroundColor(resources.getColor(android.R.color.transparent));
} else {
viewHolder.view.setBackgroundColor(readonlyBackground);
}
// name
final TextView nameView = viewHolder.nameView; {
String nameValue = task.getValue(Task.TITLE);
long hiddenUntil = task.getValue(Task.HIDE_UNTIL);
if(task.getValue(Task.DELETION_DATE) > 0) {
nameValue = resources.getString(R.string.TAd_deletedFormat, nameValue);
}
if(hiddenUntil > DateUtilities.now()) {
nameValue = resources.getString(R.string.TAd_hiddenFormat, nameValue);
}
nameView.setText(nameValue);
}
if (titleOnlyLayout) {
return;
}
float dueDateTextWidth = setupDueDateAndTags(viewHolder, task);
String details;
if(viewHolder.details1 != null) {
if(taskDetailLoader.containsKey(task.getId())) {
details = taskDetailLoader.get(task.getId()).toString();
} else {
details = task.getValue(Task.DETAILS);
}
if(TextUtils.isEmpty(details) || DETAIL_SEPARATOR.equals(details) || task.isCompleted()) {
viewHolder.details1.setVisibility(View.GONE);
viewHolder.details2.setVisibility(View.GONE);
} else if (Preferences.getBoolean(R.string.p_showNotes, false)) {
viewHolder.details1.setVisibility(View.VISIBLE);
if (details.startsWith(DETAIL_SEPARATOR)) {
StringBuffer buffer = new StringBuffer(details);
int length = DETAIL_SEPARATOR.length();
while(buffer.lastIndexOf(DETAIL_SEPARATOR, length) == 0) {
buffer.delete(0, length);
}
details = buffer.toString(); //details.substring(DETAIL_SEPARATOR.length());
}
drawDetails(viewHolder, details, dueDateTextWidth);
}
}
// Task action
ImageView taskAction = viewHolder.taskActionIcon;
if (taskAction != null) {
TaskAction action = getTaskAction(task, viewHolder.hasFiles, viewHolder.hasNotes);
if (action != null) {
taskAction.setVisibility(View.VISIBLE);
taskAction.setImageDrawable(action.icon);
taskAction.setTag(action);
} else {
taskAction.setVisibility(View.GONE);
taskAction.setTag(null);
}
}
if(Math.abs(DateUtilities.now() - task.getValue(Task.MODIFICATION_DATE)) < 2000L) {
mostRecentlyMade = task.getId();
}
}
private TaskAction getTaskAction(Task task, boolean hasFiles, boolean hasNotes) {
if (titleOnlyLayout || task.isCompleted() || !task.isEditable()) {
return null;
}
if (taskActionLoader.containsKey(task.getId())) {
return taskActionLoader.get(task.getId());
} else {
TaskAction action = LinkActionExposer.getActionsForTask(context, task, hasFiles, hasNotes);
taskActionLoader.put(task.getId(), action);
return action;
}
}
private void drawDetails(ViewHolder viewHolder, String details, float rightWidth) {
SpannableStringBuilder prospective = new SpannableStringBuilder();
SpannableStringBuilder actual = new SpannableStringBuilder();
details = details.trim().replace("\n", "<br>");
String[] splitDetails = details.split("\\|");
viewHolder.completeBox.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
rightWidth = rightWidth + viewHolder.dueDate.getPaddingRight();
float left = viewHolder.completeBox.getMeasuredWidth() +
((MarginLayoutParams)viewHolder.completeBox.getLayoutParams()).leftMargin;
int availableWidth = (int) (displayMetrics.widthPixels - left - (rightWidth + 16) * displayMetrics.density);
int i = 0;
for(; i < splitDetails.length; i++) {
Spanned spanned = convertToHtml(splitDetails[i] + " ", detailImageGetter, null);
prospective.insert(prospective.length(), spanned);
viewHolder.details1.setText(prospective);
viewHolder.details1.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
if(rightWidth > 0 && viewHolder.details1.getMeasuredWidth() > availableWidth) {
break;
}
actual.insert(actual.length(), spanned);
}
viewHolder.details1.setText(actual);
actual.clear();
if(i >= splitDetails.length) {
viewHolder.details2.setVisibility(View.GONE);
return;
} else {
viewHolder.details2.setVisibility(View.VISIBLE);
}
for(; i < splitDetails.length; i++) {
actual.insert(actual.length(), convertToHtml(splitDetails[i] + " ", detailImageGetter, null));
}
viewHolder.details2.setText(actual);
}
protected TaskRowListener listener = new TaskRowListener();
private Pair<Float, Float> lastTouchYRawY = new Pair<Float, Float>(0f, 0f);
/**
* Set listeners for this view. This is called once per view when it is
* created.
*/
protected void addListeners(final View container) {
final ViewHolder viewHolder = (ViewHolder)container.getTag();
// check box listener
OnTouchListener otl = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
lastTouchYRawY = new Pair<Float, Float>(event.getY(), event.getRawY());
return false;
}
};
viewHolder.completeBox.setOnTouchListener(otl);
viewHolder.completeBox.setOnClickListener(completeBoxListener);
if (viewHolder.picture != null) {
viewHolder.picture.setOnTouchListener(otl);
viewHolder.picture.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
viewHolder.completeBox.performClick();
}
});
}
if (viewHolder.taskActionContainer != null) {
viewHolder.taskActionContainer.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View 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
}
}
}
});
}
}
private void showEditNotesDialog(final Task task) {
String notes = null;
Task t = taskService.fetchById(task.getId(), Task.NOTES);
if (t != null) {
notes = t.getValue(Task.NOTES);
}
if (TextUtils.isEmpty(notes)) {
return;
}
int theme = ThemeService.getEditDialogTheme();
final Dialog dialog = new Dialog(fragment.getActivity(), theme);
dialog.setTitle(R.string.TEA_note_label);
View notesView = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.notes_view_dialog, null);
dialog.setContentView(notesView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
notesView.findViewById(R.id.edit_dlg_ok).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
final TextView notesField = (TextView) notesView.findViewById(R.id.notes);
notesField.setText(notes);
LayoutParams params = dialog.getWindow().getAttributes();
params.width = LayoutParams.FILL_PARENT;
params.height = LayoutParams.WRAP_CONTENT;
Configuration config = fragment.getResources().getConfiguration();
int size = config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
if (AndroidUtilities.getSdkVersion() >= 9 && size == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
DisplayMetrics metrics = fragment.getResources().getDisplayMetrics();
params.width = metrics.widthPixels / 2;
}
dialog.getWindow().setAttributes((android.view.WindowManager.LayoutParams) params);
dialog.show();
}
private void showFilesDialog(Task task) {
FilesControlSet filesControlSet = new FilesControlSet(fragment.getActivity(), R.layout.control_set_files,
R.layout.control_set_files_display, R.string.TEA_control_files);
filesControlSet.readFromTask(task);
filesControlSet.getDisplayView().performClick();
}
/* ======================================================================
* ============================================================== details
* ====================================================================== */
private final HashMap<String, Spanned> htmlCache = new HashMap<String, Spanned>(8);
private Spanned convertToHtml(String string, ImageGetter imageGetter, TagHandler tagHandler) {
if(!htmlCache.containsKey(string)) {
Spanned html;
try {
html = Html.fromHtml(string, imageGetter, tagHandler);
} catch (RuntimeException e) {
html = Spannable.Factory.getInstance().newSpannable(string);
}
htmlCache.put(string, html);
return html;
}
return htmlCache.get(string);
}
private final HashMap<Long, String> dateCache = new HashMap<Long, String>(8);
private String formatDate(long date) {
if(dateCache.containsKey(date)) {
return dateCache.get(date);
}
String formatString = "%s" + (simpleLayout ? " " : "\n") + "%s";
String string = DateUtilities.getRelativeDay(fragment.getActivity(), date);
if(Task.hasDueTime(date)) {
string = String.format(formatString, string, //$NON-NLS-1$
DateUtilities.getTimeString(fragment.getActivity(), new Date(date)));
}
dateCache.put(date, string);
return string;
}
// implementation note: this map is really costly if users have
// a large number of tasks to load, since it all goes into memory.
// it's best to do this, though, in order to append details to each other
private final Map<Long, StringBuilder> taskDetailLoader = Collections.synchronizedMap(new HashMap<Long, StringBuilder>(0));
public class DetailLoaderThread extends Thread {
@Override
public void run() {
// for all of the tasks returned by our cursor, verify details
AndroidUtilities.sleepDeep(500L);
TodorooCursor<Task> fetchCursor = taskService.fetchFiltered(
query.get(), null, Task.ID, Task.TITLE, Task.DETAILS, Task.DETAILS_DATE,
Task.MODIFICATION_DATE, Task.COMPLETION_DATE);
try {
Random random = new Random();
Task task = new Task();
for(fetchCursor.moveToFirst(); !fetchCursor.isAfterLast(); fetchCursor.moveToNext()) {
task.clear();
task.readFromCursor(fetchCursor);
if(task.isCompleted()) {
continue;
}
if(detailsAreRecentAndUpToDate(task)) {
// even if we are up to date, randomly load a fraction
if(random.nextFloat() < 0.1) {
taskDetailLoader.put(task.getId(),
new StringBuilder(task.getValue(Task.DETAILS)));
requestNewDetails(task);
if(Constants.DEBUG) {
System.err.println("Refreshing details: " + task.getId()); //$NON-NLS-1$
}
}
continue;
} else if(Constants.DEBUG) {
System.err.println("Forced loading of details: " + task.getId() + //$NON-NLS-1$
"\n details: " + new Date(task.getValue(Task.DETAILS_DATE)) + //$NON-NLS-1$
"\n modified: " + new Date(task.getValue(Task.MODIFICATION_DATE))); //$NON-NLS-1$
}
addTaskToLoadingArray(task);
task.setValue(Task.DETAILS, DETAIL_SEPARATOR);
task.setValue(Task.DETAILS_DATE, DateUtilities.now());
taskService.save(task);
requestNewDetails(task);
}
if(taskDetailLoader.size() > 0) {
Activity activity = fragment.getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
}
});
}
}
} catch (Exception e) {
// suppress silently
} finally {
fetchCursor.close();
}
}
private boolean detailsAreRecentAndUpToDate(Task task) {
return task.getValue(Task.DETAILS_DATE) >= task.getValue(Task.MODIFICATION_DATE) &&
!TextUtils.isEmpty(task.getValue(Task.DETAILS));
}
private void addTaskToLoadingArray(Task task) {
StringBuilder detailStringBuilder = new StringBuilder();
taskDetailLoader.put(task.getId(), detailStringBuilder);
}
private void requestNewDetails(Task task) {
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_REQUEST_DETAILS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
Activity activity = fragment.getActivity();
if (activity != null) {
activity.sendOrderedBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}
}
/**
* Add detail to a task
*
* @param id
* @param detail
*/
public void addDetails(long id, String detail) {
final StringBuilder details = taskDetailLoader.get(id);
if(details == null) {
return;
}
synchronized(details) {
if(details.toString().contains(detail)) {
return;
}
if(details.length() > 0) {
details.append(DETAIL_SEPARATOR);
}
details.append(detail);
Task task = new Task();
task.setId(id);
task.setValue(Task.DETAILS, details.toString());
task.setValue(Task.DETAILS_DATE, DateUtilities.now());
taskService.save(task);
}
Activity activity = fragment.getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
}
});
}
}
private final ImageGetter detailImageGetter = new ImageGetter() {
private final HashMap<Integer, Drawable> cache =
new HashMap<Integer, Drawable>(3);
@Override
public Drawable getDrawable(String source) {
int drawable = 0;
if(source.equals("silk_clock")) {
drawable = R.drawable.details_alarm;
} else if(source.equals("silk_tag_pink")) {
drawable = R.drawable.details_tag;
} else if(source.equals("silk_date")) {
drawable = R.drawable.details_repeat;
} else if(source.equals("silk_note")) {
drawable = R.drawable.details_note;
}
if (drawable == 0) {
drawable = resources.getIdentifier("drawable/" + source, null, Constants.PACKAGE);
}
if(drawable == 0) {
return null;
}
Drawable d;
if(!cache.containsKey(drawable)) {
d = resources.getDrawable(drawable);
d.setBounds(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
cache.put(drawable, d);
} else {
d = cache.get(drawable);
}
return d;
}
};
/* ======================================================================
* ============================================================== add-ons
* ====================================================================== */
/**
* Called to tell the cache to be cleared
*/
public void flushCaches() {
completedItems.clear();
decorationManager.clearCache();
taskDetailLoader.clear();
startDetailThread();
}
public HashMap<Object, Boolean> getCompletedItems() {
return completedItems;
}
/**
* AddOnManager for TaskDecorations
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class DecorationManager extends TaskAdapterAddOnManager<TaskDecoration> {
public DecorationManager() {
super(fragment);
}
private final TaskDecorationExposer[] exposers = new TaskDecorationExposer[] {
new TimerDecorationExposer(),
new NotesDecorationExposer()
};
/**
* Request add-ons for the given task
* @return true if cache miss, false if cache hit
*/
@Override
public boolean request(ViewHolder viewHolder) {
long taskId = viewHolder.task.getId();
Collection<TaskDecoration> list = initialize(taskId);
if(list != null) {
draw(viewHolder, taskId, list);
return false;
}
// request details
draw(viewHolder, taskId, get(taskId));
for(TaskDecorationExposer exposer : exposers) {
TaskDecoration deco = exposer.expose(viewHolder.task);
if(deco != null) {
addNew(viewHolder.task.getId(), exposer.getAddon(), deco, viewHolder);
}
}
return true;
}
@Override
protected void draw(ViewHolder viewHolder, long taskId, Collection<TaskDecoration> decorations) {
if(decorations == null || viewHolder.task.getId() != taskId) {
return;
}
reset(viewHolder, taskId);
if(decorations.size() == 0) {
return;
}
int i = 0;
boolean colorSet = false;
if(viewHolder.decorations == null || viewHolder.decorations.length != decorations.size()) {
viewHolder.decorations = new View[decorations.size()];
}
for(TaskDecoration decoration : decorations) {
if(decoration.color != 0 && !colorSet) {
colorSet = true;
viewHolder.view.setBackgroundColor(decoration.color);
}
if(decoration.decoration != null) {
View view = decoration.decoration.apply(fragment.getActivity(), viewHolder.taskRow);
viewHolder.decorations[i] = view;
switch(decoration.position) {
case TaskDecoration.POSITION_LEFT: {
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.BELOW, R.id.completeBox);
view.setLayoutParams(params);
viewHolder.rowBody.addView(view);
break;
}
case TaskDecoration.POSITION_RIGHT:
viewHolder.taskRow.addView(view, viewHolder.taskRow.getChildCount());
}
}
i++;
}
}
@Override
protected void reset(ViewHolder viewHolder, long taskId) {
if(viewHolder.decorations != null) {
for(View view : viewHolder.decorations) {
viewHolder.rowBody.removeView(view);
viewHolder.taskRow.removeView(view);
}
viewHolder.decorations = null;
}
if(viewHolder.task.getId() == mostRecentlyMade) {
viewHolder.view.setBackgroundColor(Color.argb(30, 150, 150, 150));
} else {
viewHolder.view.setBackgroundResource(android.R.drawable.list_selector_background);
}
}
@Override
protected Intent createBroadcastIntent(Task task) {
return null;
}
}
/* ======================================================================
* ======================================================= event handlers
* ====================================================================== */
@Override
public void notifyDataSetChanged() {
super.notifyDataSetChanged();
fontSize = Preferences.getIntegerFromString(R.string.p_fontSize, 18);
}
protected final View.OnClickListener completeBoxListener = new View.OnClickListener() {
@Override
public void onClick(View 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);
if (viewHolder.completeBox.getVisibility() == View.VISIBLE) {
viewHolder.completeBox.startAnimation(scaleAnimation);
}
}
};
protected ViewHolder getTagFromCheckBox(View v) {
return (ViewHolder)((View)v.getParent()).getTag();
}
public class TaskRowListener implements OnClickListener {
@Override
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, viewHolder.task.isEditable());
}
}
/**
* Call me when the parent presses trackpad
*/
public void onTrackpadPressed(View container) {
if(container == null) {
return;
}
final CheckBox completeBox = ((CheckBox)container.findViewById(R.id.completeBox));
completeBox.performClick();
}
/** Helper method to adjust a tasks' appearance if the task is completed or
* uncompleted.
*
* @param actionItem
* @param name
* @param progress
*/
protected void setTaskAppearance(ViewHolder viewHolder, Task task) {
Activity activity = fragment.getActivity();
if (activity == null) {
return;
}
// show item as completed if it was recently checked
Boolean value = completedItems.get(task.getUuid());
if (value == null) {
value = completedItems.get(task.getId());
}
if(value != null) {
task.setValue(Task.COMPLETION_DATE,
value ? DateUtilities.now() : 0);
}
boolean state = task.isCompleted();
TextView name = viewHolder.nameView;
if(state) {
name.setPaintFlags(name.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
name.setTextAppearance(activity, R.style.TextAppearance_TAd_ItemTitle_Completed);
} else {
name.setPaintFlags(name.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
name.setTextAppearance(activity, R.style.TextAppearance_TAd_ItemTitle);
}
name.setTextSize(fontSize);
if (!titleOnlyLayout) {
setupDueDateAndTags(viewHolder, task);
float detailTextSize = Math.max(10, fontSize * 14 / 20);
if(viewHolder.details1 != null) {
viewHolder.details1.setTextSize(detailTextSize);
}
if(viewHolder.details2 != null) {
viewHolder.details2.setTextSize(detailTextSize);
}
if(viewHolder.dueDate != null) {
viewHolder.dueDate.setTextSize(detailTextSize);
if (simpleLayout) {
viewHolder.dueDate.setTypeface(null, 0);
}
}
if (viewHolder.tagsView != null) {
viewHolder.tagsView.setTextSize(detailTextSize);
if (simpleLayout) {
viewHolder.tagsView.setTypeface(null, 0);
}
}
paint.setTextSize(detailTextSize);
// image view
final AsyncImageView pictureView = viewHolder.picture; {
if (pictureView != null) {
if(Task.USER_ID_SELF.equals(task.getValue(Task.USER_ID))) {
pictureView.setVisibility(View.GONE);
if (viewHolder.pictureBorder != null) {
viewHolder.pictureBorder.setVisibility(View.GONE);
}
} else {
pictureView.setVisibility(View.VISIBLE);
if (viewHolder.pictureBorder != null) {
viewHolder.pictureBorder.setVisibility(View.VISIBLE);
}
pictureView.setUrl(null);
if (Task.USER_ID_UNASSIGNED.equals(task.getValue(Task.USER_ID))) {
pictureView.setDefaultImageDrawable(ResourceDrawableCache.getImageDrawableFromId(resources, R.drawable.icn_anyone_transparent));
} else {
pictureView.setDefaultImageDrawable(ResourceDrawableCache.getImageDrawableFromId(resources, R.drawable.icn_default_person_image));
if (!TextUtils.isEmpty(viewHolder.imageUrl)) {
pictureView.setUrl(viewHolder.imageUrl);
} else if (!TextUtils.isEmpty(task.getValue(Task.USER))) {
try {
JSONObject user = new JSONObject(task.getValue(Task.USER));
pictureView.setUrl(user.optString("picture")); //$NON-NLS-1$
} catch (JSONException e) {
//
}
}
}
}
}
}
}
setupCompleteBox(viewHolder);
}
private void setupCompleteBox(ViewHolder viewHolder) {
// complete box
final Task task = viewHolder.task;
final AsyncImageView pictureView = viewHolder.picture;
final CheckableImageView checkBoxView = viewHolder.completeBox; {
boolean completed = task.isCompleted();
checkBoxView.setChecked(completed);
// disable checkbox if task is readonly
checkBoxView.setEnabled(viewHolder.task.isEditable());
int value = task.getValue(Task.IMPORTANCE);
if (value >= IMPORTANCE_RESOURCES.length) {
value = IMPORTANCE_RESOURCES.length - 1;
}
Drawable[] boxes = IMPORTANCE_DRAWABLES;
if (!TextUtils.isEmpty(task.getValue(Task.RECURRENCE))) {
boxes = completed ? IMPORTANCE_REPEAT_DRAWABLES_CHECKED : IMPORTANCE_REPEAT_DRAWABLES;
} else {
boxes = completed ? IMPORTANCE_DRAWABLES_CHECKED : IMPORTANCE_DRAWABLES;
}
checkBoxView.setImageDrawable(boxes[value]);
if (titleOnlyLayout) {
return;
}
if (checkBoxView.isChecked()) {
if (pictureView != null) {
pictureView.setVisibility(View.GONE);
}
if (viewHolder.pictureBorder != null) {
viewHolder.pictureBorder.setVisibility(View.GONE);
}
}
if (pictureView != null && pictureView.getVisibility() == View.VISIBLE) {
checkBoxView.setVisibility(View.INVISIBLE);
if (viewHolder.pictureBorder != null) {
viewHolder.pictureBorder.setBackgroundDrawable(IMPORTANCE_DRAWABLES_LARGE[value]);
}
} else {
checkBoxView.setVisibility(View.VISIBLE);
}
}
}
// Returns due date text width
private float setupDueDateAndTags(ViewHolder viewHolder, Task task) {
// due date / completion date
float dueDateTextWidth = 0;
final TextView dueDateView = viewHolder.dueDate; {
Activity activity = fragment.getActivity();
if (activity != null) {
if(!task.isCompleted() && task.hasDueDate()) {
long dueDate = task.getValue(Task.DUE_DATE);
if(task.isOverdue()) {
dueDateView.setTextAppearance(activity, R.style.TextAppearance_TAd_ItemDueDate_Overdue);
} else {
dueDateView.setTextAppearance(activity, R.style.TextAppearance_TAd_ItemDueDate);
}
String dateValue = formatDate(dueDate);
dueDateView.setText(dateValue);
dueDateTextWidth = paint.measureText(dateValue);
dueDateView.setVisibility(View.VISIBLE);
} else if(task.isCompleted()) {
String dateValue = formatDate(task.getValue(Task.COMPLETION_DATE));
dueDateView.setText(resources.getString(R.string.TAd_completed, dateValue));
dueDateView.setTextAppearance(activity, R.style.TextAppearance_TAd_ItemDueDate_Completed);
dueDateTextWidth = paint.measureText(dateValue);
dueDateView.setVisibility(View.VISIBLE);
} else {
dueDateView.setVisibility(View.GONE);
}
if (viewHolder.tagsView != null) {
String tags = viewHolder.tagsString;
if (tags != null && task.hasDueDate()) {
tags = " | " + tags; //$NON-NLS-1$
}
if (!task.isCompleted()) {
viewHolder.tagsView.setText(tags);
viewHolder.tagsView.setVisibility(TextUtils.isEmpty(tags) ? View.GONE : View.VISIBLE);
} else {
viewHolder.tagsView.setText(""); //$NON-NLS-1$
viewHolder.tagsView.setVisibility(View.GONE);
}
}
}
}
return dueDateTextWidth;
}
/**
* This method is called when user completes a task via check box or other
* means
*
* @param container
* container for the action item
* @param newState
* state that this task should be set to
* @param completeBox
* the box that was clicked. can be null
*/
protected void completeTask(final Task task, final boolean newState) {
if(task == null) {
return;
}
if (newState != task.isCompleted()) {
if(onCompletedTaskListener != null) {
onCompletedTaskListener.onCompletedTask(task, newState);
}
completedItems.put(task.getUuid(), newState);
taskService.setComplete(task, newState);
}
}
/**
* Add a new listener
* @param newListener
*/
public void addOnCompletedTaskListener(final OnCompletedTaskListener newListener) {
if(this.onCompletedTaskListener == null) {
this.onCompletedTaskListener = newListener;
} else {
final OnCompletedTaskListener old = this.onCompletedTaskListener;
this.onCompletedTaskListener = new OnCompletedTaskListener() {
@Override
public void onCompletedTask(Task item, boolean newState) {
old.onCompletedTask(item, newState);
newListener.onCompletedTask(item, newState);
}
};
}
}
}