From 065fc1a9d4130d119f82f785f46ce150e7c1b3df Mon Sep 17 00:00:00 2001 From: Tim Su Date: Fri, 26 Dec 2008 06:20:26 +0000 Subject: [PATCH] First pass at adding tagging features to astrid. Add tags from the editor view, also a tags listing page to view the tags you have. --- AndroidManifest.xml | 2 + res/layout/edit_tag_item.xml | 38 ++ res/layout/tag_list.xml | 12 + res/layout/task_edit.xml | 4 +- res/layout/task_list_row.xml | 13 +- res/values/strings.xml | 13 + src/com/timsu/astrid/activities/TagList.java | 231 +++++++++ src/com/timsu/astrid/activities/TaskEdit.java | 450 +++++++++--------- src/com/timsu/astrid/activities/TaskList.java | 109 ++++- .../activities/TaskModificationActivity.java | 25 +- src/com/timsu/astrid/activities/TaskView.java | 6 +- .../timsu/astrid/data/AbstractController.java | 63 ++- src/com/timsu/astrid/data/Identifier.java | 31 ++ .../astrid/data/tag/AbstractTagModel.java | 177 +++++++ .../timsu/astrid/data/tag/TagController.java | 202 ++++++++ .../timsu/astrid/data/tag/TagIdentifier.java | 12 + .../astrid/data/tag/TagModelForView.java | 42 ++ .../astrid/data/tag/TagToTaskMapping.java | 116 +++++ .../astrid/data/task/AbstractTaskModel.java | 6 +- .../astrid/data/task/TaskController.java | 52 +- .../astrid/data/task/TaskIdentifier.java | 12 +- .../astrid/data/task/TaskModelForList.java | 25 +- .../timsu/astrid/widget/DateControlSet.java | 120 +++++ .../astrid/widget/TimeDurationControlSet.java | 79 +++ 24 files changed, 1554 insertions(+), 286 deletions(-) create mode 100644 res/layout/edit_tag_item.xml create mode 100644 res/layout/tag_list.xml create mode 100644 src/com/timsu/astrid/activities/TagList.java create mode 100644 src/com/timsu/astrid/data/Identifier.java create mode 100644 src/com/timsu/astrid/data/tag/AbstractTagModel.java create mode 100644 src/com/timsu/astrid/data/tag/TagController.java create mode 100644 src/com/timsu/astrid/data/tag/TagIdentifier.java create mode 100644 src/com/timsu/astrid/data/tag/TagModelForView.java create mode 100644 src/com/timsu/astrid/data/tag/TagToTaskMapping.java create mode 100644 src/com/timsu/astrid/widget/DateControlSet.java create mode 100644 src/com/timsu/astrid/widget/TimeDurationControlSet.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f34c60e7a..1baed86ab 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -17,5 +17,7 @@ + + \ No newline at end of file diff --git a/res/layout/edit_tag_item.xml b/res/layout/edit_tag_item.xml new file mode 100644 index 000000000..4f57635e9 --- /dev/null +++ b/res/layout/edit_tag_item.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/res/layout/tag_list.xml b/res/layout/tag_list.xml new file mode 100644 index 000000000..1a28081d8 --- /dev/null +++ b/res/layout/tag_list.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/res/layout/task_edit.xml b/res/layout/task_edit.xml index 218367ef4..e12350838 100644 --- a/res/layout/task_edit.xml +++ b/res/layout/task_edit.xml @@ -82,7 +82,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" /> - + diff --git a/res/layout/task_list_row.xml b/res/layout/task_list_row.xml index 750581779..d9f83650a 100644 --- a/res/layout/task_list_row.xml +++ b/res/layout/task_list_row.xml @@ -9,6 +9,7 @@ android:layout_height="wrap_content"> @@ -18,13 +19,21 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:paddingLeft="5dip"/> - + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index d31919efc..560c30fb5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -17,6 +17,10 @@ 1 Task %d Tasks + + 1 Tag + %d Tags + 1 Day @@ -38,6 +42,7 @@ Astrid: + Tasks Tagged \"%s\": hidden New Task @@ -96,10 +101,18 @@ Task Notes Overdue % of Task Finished + + + + Astrid: Tag View: + Create Task With Tag + Edit Tag + Delete Tag Delete Delete this task? + Remove this tag from all tasks? diff --git a/src/com/timsu/astrid/activities/TagList.java b/src/com/timsu/astrid/activities/TagList.java new file mode 100644 index 000000000..a11490628 --- /dev/null +++ b/src/com/timsu/astrid/activities/TagList.java @@ -0,0 +1,231 @@ +/* + * ASTRID: Android's Simple Task Recording Dashboard + * + * Copyright (c) 2009 Tim Su + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.timsu.astrid.activities; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View.OnCreateContextMenuListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; + +import com.timsu.astrid.R; +import com.timsu.astrid.data.tag.TagController; +import com.timsu.astrid.data.tag.TagIdentifier; +import com.timsu.astrid.data.tag.TagModelForView; +import com.timsu.astrid.data.task.TaskIdentifier; + + +/** List all tags and allows a user to see all tasks for a given tag + * + * @author Tim Su (timsu@stanfordalumni.org) + * + */ +public class TagList extends Activity { + private static final int ACTIVITY_LIST = 0; + private static final int ACTIVITY_CREATE = 1; + + private static final int CONTEXT_CREATE_ID = Menu.FIRST + 10; + private static final int CONTEXT_DELETE_ID = Menu.FIRST + 11; + + private TagController controller; + private ListView listView; + + private List tagArray; + + /** Called when loading up the activity for the first time */ + private void onLoad() { + controller = new TagController(this); + controller.open(); + + listView = (ListView)findViewById(R.id.taglist); + + fillData(); + } + + /** Fill in the Tag List with our tags */ + private void fillData() { + Resources r = getResources(); + + tagArray = controller.getAllTags(); + + // set up the title + StringBuilder title = new StringBuilder(). + append(r.getString(R.string.tagList_titlePrefix)). + append(" ").append(r.getQuantityString(R.plurals.Ntags, + tagArray.size(), tagArray.size())); + setTitle(title); + + // set up our adapter + TagListAdapter tagAdapter = new TagListAdapter(this, + android.R.layout.simple_list_item_1, tagArray); + listView.setAdapter(tagAdapter); + + // list view listener + listView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, + int position, long id) { + TagModelForView tag = (TagModelForView)view.getTag(); + + Intent intent = new Intent(TagList.this, TaskList.class); + intent.putExtra(TaskList.TAG_TOKEN, tag. + getTagIdentifier().getId()); + startActivityForResult(intent, ACTIVITY_LIST); + } + }); + + listView.setOnCreateContextMenuListener(new OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + AdapterContextMenuInfo adapterMenuInfo = + (AdapterContextMenuInfo)menuInfo; + int position = adapterMenuInfo.position; + + menu.add(position, CONTEXT_CREATE_ID, Menu.NONE, + R.string.tagList_context_create); + menu.add(position, CONTEXT_DELETE_ID, Menu.NONE, + R.string.tagList_context_delete); + + menu.setHeaderTitle(tagArray.get(position).getName()); + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent intent) { + fillData(); + } + + // --- list adapter + + private class TagListAdapter extends ArrayAdapter { + + private List objects; + private int resource; + private LayoutInflater inflater; + + public TagListAdapter(Context context, int resource, + List objects) { + super(context, resource, objects); + + inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + this.objects = objects; + this.resource = resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view; + + view = inflater.inflate(resource, parent, false); + setupView(view, objects.get(position)); + + return view; + } + + public void setupView(View view, final TagModelForView tag) { + // set up basic properties + view.setTag(tag); + + List tasks = controller.getTaggedTasks( + tag.getTagIdentifier()); + + final TextView name = ((TextView)view.findViewById(android.R.id.text1)); + name.setText(new StringBuilder(tag.getName()). + append(" (").append(tasks.size()).append(")")); + } + } + + // --- ui control handlers + + private void createTask(TagModelForView tag) { + Intent intent = new Intent(this, TaskEdit.class); + intent.putExtra(TaskEdit.TAG_NAME_TOKEN, tag.getName()); + startActivityForResult(intent, ACTIVITY_CREATE); + } + + private void deleteTag(final TagIdentifier tagId) { + new AlertDialog.Builder(this) + .setTitle(R.string.delete_title) + .setMessage(R.string.delete_this_tag_title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + controller.deleteTag(tagId); + fillData(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + switch(item.getItemId()) { + case CONTEXT_CREATE_ID: + TagModelForView tag = tagArray.get(item.getGroupId()); + createTask(tag); + return true; + case CONTEXT_DELETE_ID: + tag = tagArray.get(item.getGroupId()); + deleteTag(tag.getTagIdentifier()); + return true; + } + + return super.onMenuItemSelected(featureId, item); + } + + // --- creating stuff + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.tag_list); + + onLoad(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + controller.close(); + } +} \ No newline at end of file diff --git a/src/com/timsu/astrid/activities/TaskEdit.java b/src/com/timsu/astrid/activities/TaskEdit.java index ab1e9f1ef..3ff90ba31 100644 --- a/src/com/timsu/astrid/activities/TaskEdit.java +++ b/src/com/timsu/astrid/activities/TaskEdit.java @@ -1,5 +1,5 @@ /* - * ASTRID: Android's Simple Task Recording Dame + * ASTRID: Android's Simple Task Recording Dashboard * * Copyright (c) 2009 Tim Su * @@ -19,19 +19,20 @@ */ package com.timsu.astrid.activities; -import java.text.Format; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; import android.app.AlertDialog; -import android.app.DatePickerDialog; -import android.app.TimePickerDialog; -import android.app.DatePickerDialog.OnDateSetListener; -import android.app.TimePickerDialog.OnTimeSetListener; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -39,32 +40,41 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.DatePicker; import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; -import android.widget.TimePicker; -import android.widget.CompoundButton.OnCheckedChangeListener; import com.timsu.astrid.R; import com.timsu.astrid.data.enums.Importance; +import com.timsu.astrid.data.tag.TagController; +import com.timsu.astrid.data.tag.TagIdentifier; +import com.timsu.astrid.data.tag.TagModelForView; import com.timsu.astrid.data.task.TaskIdentifier; import com.timsu.astrid.data.task.TaskModelForEdit; -import com.timsu.astrid.utilities.DateUtilities; -import com.timsu.astrid.widget.NumberPicker; -import com.timsu.astrid.widget.NumberPickerDialog; -import com.timsu.astrid.widget.NumberPickerDialog.OnNumberPickedListener; +import com.timsu.astrid.widget.DateControlSet; +import com.timsu.astrid.widget.TimeDurationControlSet; public class TaskEdit extends TaskModificationActivity { - private static final int SAVE_ID = Menu.FIRST; - private static final int DISCARD_ID = Menu.FIRST + 1; - private static final int DELETE_ID = Menu.FIRST + 2; - public static final int RESULT_DELETE = RESULT_FIRST_USER; + // bundle arguments + public static final String TAG_NAME_TOKEN = "tag"; + // menu items + private static final int SAVE_ID = Menu.FIRST; + private static final int DISCARD_ID = Menu.FIRST + 1; + private static final int DELETE_ID = Menu.FIRST + 2; + + // activity results + public static final int RESULT_DELETE = RESULT_FIRST_USER; + + // other constants + private static final int MAX_TAGS = 5; + + // UI components private EditText name; private Spinner importance; private TimeDurationControlSet estimatedDuration; @@ -73,19 +83,29 @@ public class TaskEdit extends TaskModificationActivity { private DateControlSet preferredDueDate; private DateControlSet hiddenUntil; private EditText notes; + private LinearLayout tagsContainer; + // other instance variables private boolean shouldSaveState = true; + private TagController tagController; + private List tags; + private List taskTags; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + tagController = new TagController(this); + tagController.open(); setContentView(R.layout.task_edit); setUpUIComponents(); setUpListeners(); - populateFields(); - } + Bundle extras = getIntent().getExtras(); + if(extras != null && extras.containsKey(TAG_NAME_TOKEN)) { + addTag(extras.getString(TAG_NAME_TOKEN)); + } + } @Override protected TaskModelForEdit getModel(TaskIdentifier identifier) { if (identifier != null) @@ -94,80 +114,141 @@ public class TaskEdit extends TaskModificationActivity { return controller.createNewTaskForEdit(); } - // --- data saving and retrieving + /* ====================================================================== + * =============================================== model reading / saving + * ====================================================================== */ private void populateFields() { Resources r = getResources(); + + // set UI components based on model variables if(model.getCursor() != null) startManagingCursor(model.getCursor()); - name.setText(model.getName()); if(model.getName().length() > 0) setTitle(new StringBuilder(). append(r.getString(R.string.taskEdit_titlePrefix)). append(" "). append(model.getName())); - estimatedDuration.setTimeElapsed(model.getEstimatedSeconds()); elapsedDuration.setTimeElapsed(model.getElapsedSeconds()); importance.setSelection(model.getImportance().ordinal()); - definiteDueDate.setDate(model.getDefiniteDueDate()); preferredDueDate.setDate(model.getPreferredDueDate()); hiddenUntil.setDate(model.getHiddenUntil()); - notes.setText(model.getNotes()); + + // tags + tags = tagController.getAllTags(); + if(model.getTaskIdentifier() != null) { + taskTags = tagController.getTaskTags(model.getTaskIdentifier()); + if(taskTags.size() > 0) { + Map tagsMap = + new HashMap(); + for(TagModelForView tag : tags) + tagsMap.put(tag.getTagIdentifier(), tag); + for(TagIdentifier id : taskTags) { + if(!tagsMap.containsKey(id)) + continue; + + TagModelForView tag = tagsMap.get(id.getId()); + addTag(tag.getName()); + } + } + } else + taskTags = new LinkedList(); + + addTag(""); } private void save() { - // usually, user accidentally created a new task + // don't save if user accidentally created a new task if(name.getText().length() == 0) return; model.setName(name.getText().toString()); model.setEstimatedSeconds(estimatedDuration.getTimeDurationInSeconds()); model.setElapsedSeconds(elapsedDuration.getTimeDurationInSeconds()); - model.setImportance(Importance.values()[importance.getSelectedItemPosition()]); - + model.setImportance(Importance.values() + [importance.getSelectedItemPosition()]); model.setDefiniteDueDate(definiteDueDate.getDate()); model.setPreferredDueDate(preferredDueDate.getDate()); model.setHiddenUntil(hiddenUntil.getDate()); - model.setNotes(notes.getText().toString()); try { - if(!controller.saveTask(model)) - throw new RuntimeException("Unable to save task: false"); - } catch (RuntimeException e) { - Log.e(getClass().getSimpleName(), "Error saving task!", e); + // write out to database + controller.saveTask(model); + saveTags(); + } catch (Exception e) { + Log.e(getClass().getSimpleName(), "Error saving task!", e); // TODO + } + + } + + /** Save task tags. Must be called after task already has an ID */ + private void saveTags() { + Set tagsToDelete; + Set tagsToAdd; + + HashSet tagNames = new HashSet(); + for(int i = 0; i < tagsContainer.getChildCount(); i++) { + TextView tagName = (TextView)tagsContainer.getChildAt(i).findViewById(R.id.text1); + if(tagName.getText().length() == 0) + continue; + tagNames.add(tagName.getText().toString()); + } + + // map names to tag identifiers, creating them if necessary + HashSet tagIds = new HashSet(); + HashMap tagsByName = new HashMap(); + for(TagModelForView tag : tags) + tagsByName.put(tag.getName(), tag.getTagIdentifier()); + for(String tagName : tagNames) { + if(tagsByName.containsKey(tagName)) + tagIds.add(tagsByName.get(tagName)); + else { + TagIdentifier newTagId = tagController.createTag(tagName); + tagIds.add(newTagId); + } } + + tagsToDelete = new HashSet(taskTags); + tagsToDelete.removeAll(tagIds); + tagsToAdd = tagIds; + tagsToAdd.removeAll(taskTags); + + for(TagIdentifier tagId : tagsToDelete) + tagController.removeTag(model.getTaskIdentifier(), tagId); + for(TagIdentifier tagId : tagsToAdd) + tagController.addTag(model.getTaskIdentifier(), tagId); } - // --- user interface components + /* ====================================================================== + * ==================================================== UI initialization + * ====================================================================== */ + /** Initialize UI components */ private void setUpUIComponents() { Resources r = getResources(); setTitle(new StringBuilder() - .append(r.getString(R.string.app_name)) - .append(": ") .append(r.getString(R.string.taskEdit_titleGeneric))); + // populate instance variables name = (EditText)findViewById(R.id.name); importance = (Spinner)findViewById(R.id.importance); - - estimatedDuration = new TimeDurationControlSet(R.id.estimatedDuration); - elapsedDuration = new TimeDurationControlSet(R.id.elapsedDuration); - definiteDueDate = new DateControlSet(R.id.definiteDueDate_notnull, + tagsContainer = (LinearLayout)findViewById(R.id.tags_container); + estimatedDuration = new TimeDurationControlSet(this, R.id.estimatedDuration); + elapsedDuration = new TimeDurationControlSet(this, R.id.elapsedDuration); + definiteDueDate = new DateControlSet(this, R.id.definiteDueDate_notnull, R.id.definiteDueDate_date, R.id.definiteDueDate_time); - preferredDueDate = new DateControlSet(R.id.preferredDueDate_notnull, + preferredDueDate = new DateControlSet(this, R.id.preferredDueDate_notnull, R.id.preferredDueDate_date, R.id.preferredDueDate_time); - hiddenUntil = new DateControlSet(R.id.hiddenUntil_notnull, + hiddenUntil = new DateControlSet(this, R.id.hiddenUntil_notnull, R.id.hiddenUntil_date, R.id.hiddenUntil_time); - notes = (EditText)findViewById(R.id.notes); - // set up for each field - + // individual ui component initialization ImportanceAdapter importanceAdapter = new ImportanceAdapter(this, android.R.layout.simple_spinner_item, R.layout.importance_spinner_dropdown, @@ -175,61 +256,8 @@ public class TaskEdit extends TaskModificationActivity { importance.setAdapter(importanceAdapter); } - /** Display importance with proper formatting */ - private class ImportanceAdapter extends ArrayAdapter { - private int textViewResourceId, dropDownResourceId; - private LayoutInflater inflater; - - public ImportanceAdapter(Context context, int textViewResourceId, - int dropDownResourceId, Importance[] objects) { - super(context, textViewResourceId, objects); - - inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - this.textViewResourceId = textViewResourceId; - this.dropDownResourceId = dropDownResourceId; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - return getView(position, convertView, parent, textViewResourceId, true); - } - - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - return getView(position, convertView, parent, dropDownResourceId, true); - } - - public View getView(int position, View convertView, ViewGroup parent, - int resource, boolean setColors) { - View view; - TextView text; - Resources r = getResources(); - - if (convertView == null) { - view = inflater.inflate(resource, parent, false); - } else { - view = convertView; - } - - try { - text = (TextView) view; - } catch (ClassCastException e) { - Log.e("ArrayAdapter", "You must supply a resource ID for a TextView"); - throw new IllegalStateException( - "ArrayAdapter requires the resource ID to be a TextView", e); - } - - text.setText(r.getString(getItem(position).getLabelResource())); - if(setColors) - text.setBackgroundColor(r.getColor(getItem(position).getColorResource())); - - return view; - } - } - /** Set up button listeners */ private void setUpListeners() { - Button saveButton = (Button) findViewById(R.id.save); saveButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { @@ -247,13 +275,70 @@ public class TaskEdit extends TaskModificationActivity { Button deleteButton = (Button) findViewById(R.id.delete); if(model.getTaskIdentifier() == null) deleteButton.setVisibility(View.GONE); - deleteButton.setOnClickListener(new View.OnClickListener() { - public void onClick(View view) { - deleteButtonClick(); + else { + deleteButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + deleteButtonClick(); + } + }); + } + } + + /** Adds a tag to the tag field */ + private boolean addTag(String tagName) { + if (tagsContainer.getChildCount() >= MAX_TAGS) { + return false; + } + + LayoutInflater inflater = getLayoutInflater(); + final View tagItem = inflater.inflate(R.layout.edit_tag_item, null); + tagsContainer.addView(tagItem); + + AutoCompleteTextView textView = (AutoCompleteTextView)tagItem. + findViewById(R.id.text1); + textView.setText(tagName); + ArrayAdapter tagsAdapter = + new ArrayAdapter(this, + android.R.layout.simple_dropdown_item_1line, tags); + textView.setAdapter(tagsAdapter); + textView.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + if(start == 0 && tagsContainer.getChildAt( + tagsContainer.getChildCount()-1) == tagItem) { + addTag(""); + } + } + + @Override + public void afterTextChanged(Editable s) { + // + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + // } }); + + ImageButton reminderRemoveButton; + reminderRemoveButton = (ImageButton)tagItem.findViewById(R.id.button1); + reminderRemoveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + tagsContainer.removeView(tagItem); + } + }); + + return true; } + /* ====================================================================== + * ======================================================= event handlers + * ====================================================================== */ + private void saveButtonClick() { setResult(RESULT_OK); finish(); @@ -342,136 +427,65 @@ public class TaskEdit extends TaskModificationActivity { populateFields(); } - // --- date/time methods and helper classes - - private class TimeDurationControlSet implements OnNumberPickedListener, - View.OnClickListener { - private Button timeButton; - private int timeDuration; - private final NumberPickerDialog dialog = - new NumberPickerDialog(TaskEdit.this, this, - getResources().getString(R.string.minutes_dialog), - 0, 5, 0, 999); - - public TimeDurationControlSet(int timeButtonId) { - timeButton = (Button)findViewById(timeButtonId); - timeButton.setOnClickListener(this); - } - - public int getTimeDurationInSeconds() { - return timeDuration; - } + @Override + protected void onDestroy() { + super.onDestroy(); + tagController.close(); + } - public void setTimeElapsed(Integer timeDurationInSeconds) { - if(timeDurationInSeconds == null) - timeDurationInSeconds = 0; + /* ====================================================================== + * ========================================== UI component helper classes + * ====================================================================== */ - timeDuration = timeDurationInSeconds; + /** Adapter with custom view to display Importance with proper formatting */ + private class ImportanceAdapter extends ArrayAdapter { + private int textViewResourceId, dropDownResourceId; + private LayoutInflater inflater; - Resources r = getResources(); - if(timeDurationInSeconds == 0) { - timeButton.setText(r.getString(R.string.blank_button_title)); - return; - } + public ImportanceAdapter(Context context, int textViewResourceId, + int dropDownResourceId, Importance[] objects) { + super(context, textViewResourceId, objects); - timeButton.setText(DateUtilities.getDurationString(r, - timeDurationInSeconds, 2)); - dialog.setInitialValue(timeDuration/60); + inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + this.textViewResourceId = textViewResourceId; + this.dropDownResourceId = dropDownResourceId; } @Override - /** Called when NumberPicker activity is completed */ - public void onNumberPicked(NumberPicker view, int value) { - setTimeElapsed(value * 60); - } - - /** Called when time button is clicked */ - public void onClick(View v) { - dialog.show(); + public View getView(int position, View convertView, ViewGroup parent) { + return getView(position, convertView, parent, textViewResourceId, true); } - - } - - private static final Format dateFormatter = new SimpleDateFormat("EEE, MMM d, yyyy"); - private static final Format timeFormatter = new SimpleDateFormat("h:mm a"); - - private class DateControlSet implements OnTimeSetListener, - OnDateSetListener, View.OnClickListener { - private CheckBox activatedCheckBox; - private Button dateButton; - private Button timeButton; - private Date date; - - public DateControlSet(int checkBoxId, int dateButtonId, int timeButtonId) { - activatedCheckBox = (CheckBox)findViewById(checkBoxId); - dateButton = (Button)findViewById(dateButtonId); - timeButton = (Button)findViewById(timeButtonId); - - activatedCheckBox.setOnCheckedChangeListener( - new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, - boolean isChecked) { - dateButton.setEnabled(isChecked); - timeButton.setEnabled(isChecked); - } - }); - dateButton.setOnClickListener(this); - timeButton.setOnClickListener(this); + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return getView(position, convertView, parent, dropDownResourceId, true); } - public Date getDate() { - if(!activatedCheckBox.isChecked()) - return null; - return date; - } + public View getView(int position, View convertView, ViewGroup parent, + int resource, boolean setColors) { + View view; + TextView text; + Resources r = getResources(); - /** Initialize the components for the given date field */ - public void setDate(Date newDate) { - this.date = newDate; - if(newDate == null) { - date = new Date(); - date.setMinutes(0); + if (convertView == null) { + view = inflater.inflate(resource, parent, false); + } else { + view = convertView; } - activatedCheckBox.setChecked(newDate != null); - dateButton.setEnabled(newDate != null); - timeButton.setEnabled(newDate != null); - - updateDate(); - updateTime(); - } - - public void onDateSet(DatePicker view, int year, int month, int monthDay) { - date.setYear(year - 1900); - date.setMonth(month); - date.setDate(monthDay); - updateDate(); - } - - public void onTimeSet(TimePicker view, int hourOfDay, int minute) { - date.setHours(hourOfDay); - date.setMinutes(minute); - updateTime(); - } - - public void updateDate() { - dateButton.setText(dateFormatter.format(date)); - - } + try { + text = (TextView) view; + } catch (ClassCastException e) { + Log.e("ArrayAdapter", "You must supply a resource ID for a TextView"); + throw new IllegalStateException( + "ArrayAdapter requires the resource ID to be a TextView", e); + } - public void updateTime() { - timeButton.setText(timeFormatter.format(date)); - } + text.setText(r.getString(getItem(position).getLabelResource())); + if(setColors) + text.setBackgroundColor(r.getColor(getItem(position).getColorResource())); - public void onClick(View v) { - if(v == timeButton) - new TimePickerDialog(TaskEdit.this, this, date.getHours(), - date.getMinutes(), false).show(); - else - new DatePickerDialog(TaskEdit.this, this, 1900 + - date.getYear(), date.getMonth(), date.getDate()).show(); + return view; } } } diff --git a/src/com/timsu/astrid/activities/TaskList.java b/src/com/timsu/astrid/activities/TaskList.java index b3f68d9cd..e9a007bf7 100644 --- a/src/com/timsu/astrid/activities/TaskList.java +++ b/src/com/timsu/astrid/activities/TaskList.java @@ -19,7 +19,9 @@ */ package com.timsu.astrid.activities; import java.util.Date; +import java.util.Iterator; import java.util.List; +import java.util.Map; import android.app.Activity; import android.app.AlertDialog; @@ -49,6 +51,9 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.CompoundButton.OnCheckedChangeListener; import com.timsu.astrid.R; +import com.timsu.astrid.data.tag.TagController; +import com.timsu.astrid.data.tag.TagIdentifier; +import com.timsu.astrid.data.tag.TagModelForView; import com.timsu.astrid.data.task.TaskController; import com.timsu.astrid.data.task.TaskIdentifier; import com.timsu.astrid.data.task.TaskModelForList; @@ -61,36 +66,56 @@ import com.timsu.astrid.data.task.TaskModelForList; * */ public class TaskList extends Activity { + + // bundle tokens + public static final String TAG_TOKEN = "tag"; + + // activities private static final int ACTIVITY_CREATE = 0; private static final int ACTIVITY_VIEW = 1; private static final int ACTIVITY_EDIT = 2; + private static final int ACTIVITY_TAGS = 3; + // menu ids private static final int INSERT_ID = Menu.FIRST; private static final int FILTERS_ID = Menu.FIRST + 1; private static final int TAGS_ID = Menu.FIRST + 2; - private static final int CONTEXT_EDIT_ID = Menu.FIRST + 10; private static final int CONTEXT_DELETE_ID = Menu.FIRST + 11; private static final int CONTEXT_TIMER_ID = Menu.FIRST + 12; - private static final int CONTEXT_FILTER_HIDDEN = Menu.FIRST + 20; private static final int CONTEXT_FILTER_DONE = Menu.FIRST + 21; + // ui components private TaskController controller; + private TagController tagController = null; private ListView listView; private Button addButton; + // other instance variables private List taskArray; + private Map tagMap; private boolean filterShowHidden = false; private boolean filterShowDone = false; + private TagModelForView filterTag = null; /** Called when loading up the activity for the first time */ private void onLoad() { controller = new TaskController(this); controller.open(); + tagController = new TagController(this); + tagController.open(); - listView = (ListView)findViewById(R.id.tasklist); + tagMap = tagController.getAllTagsAsMap(); + // check if we want to filter by tag + Bundle extras = getIntent().getExtras(); + if(extras != null && extras.containsKey(TAG_TOKEN)) { + TagIdentifier identifier = new TagIdentifier(extras.getLong(TAG_TOKEN)); + filterTag = tagMap.get(identifier); + } + + listView = (ListView)findViewById(R.id.tasklist); addButton = (Button)findViewById(R.id.addtask); addButton.setOnClickListener(new View.OnClickListener() { @@ -129,17 +154,25 @@ public class TaskList extends Activity { private void fillData() { Resources r = getResources(); - // get the database cursor Cursor tasksCursor; - if(filterShowDone) - tasksCursor = controller.getAllTaskListCursor(); - else - tasksCursor = controller.getActiveTaskListCursor(); + + // get the array of tasks + if(filterTag != null) { + List tasks = tagController.getTaggedTasks( + filterTag.getTagIdentifier()); + + tasksCursor = controller.getTaskListCursorById(tasks); + } else { + if(filterShowDone) + tasksCursor = controller.getAllTaskListCursor(); + else + tasksCursor = controller.getActiveTaskListCursor(); + } startManagingCursor(tasksCursor); - int totalTasks = tasksCursor.getCount(); - taskArray = controller.createTaskListFromCursor(tasksCursor, !filterShowHidden); - int hiddenTasks = totalTasks - taskArray.size(); + taskArray = controller.createTaskListFromCursor(tasksCursor, + !filterShowHidden); + int hiddenTasks = tasksCursor.getCount() - taskArray.size(); // hide "add" button if we have a few tasks if(taskArray.size() > 2) @@ -147,8 +180,12 @@ public class TaskList extends Activity { // set up the title StringBuilder title = new StringBuilder(). - append(r.getString(R.string.taskList_titlePrefix)). - append(" ").append(r.getQuantityString(R.plurals.Ntasks, + append(r.getString(R.string.taskList_titlePrefix)).append(" "); + if(filterTag != null) { + title.append(r.getString(R.string.taskList_titleTagPrefix, + filterTag.getName())).append(" "); + } + title.append(r.getQuantityString(R.plurals.Ntasks, taskArray.size(), taskArray.size())); if(hiddenTasks > 0) title.append(" (").append(hiddenTasks).append(" "). @@ -215,6 +252,7 @@ public class TaskList extends Activity { // set up basic properties final TextView name = ((TextView)view.findViewById(R.id.text1)); + final TextView properties = ((TextView)view.findViewById(R.id.text2)); final CheckBox progress = ((CheckBox)view.findViewById(R.id.cb1)); final ImageView timer = ((ImageView)view.findViewById(R.id.image1)); @@ -226,6 +264,21 @@ public class TaskList extends Activity { if(task.getTimerStart() != null) timer.setImageDrawable(r.getDrawable(R.drawable.ic_dialog_time)); progress.setChecked(task.isTaskCompleted()); + + List tags = tagController.getTaskTags( + task.getTaskIdentifier()); + StringBuilder tagString = new StringBuilder(); + for(Iterator i = tags.iterator(); i.hasNext(); ) { + TagModelForView tag = tagMap.get(i.next()); + tagString.append(tag.getName()); + if(i.hasNext()) + tagString.append(", "); + } + if(tagString.length() > 0) + properties.setText(tagString); + else + properties.setVisibility(View.GONE); + setTaskAppearance(name, task); } @@ -302,8 +355,10 @@ public class TaskList extends Activity { // --- ui control handlers private void createTask() { - Intent i = new Intent(this, TaskEdit.class); - startActivityForResult(i, ACTIVITY_CREATE); + Intent intent = new Intent(this, TaskEdit.class); + if(filterTag != null) + intent.putExtra(TaskEdit.TAG_NAME_TOKEN, filterTag.getName()); + startActivityForResult(intent, ACTIVITY_CREATE); } private void deleteTask(final TaskIdentifier taskId) { @@ -325,6 +380,9 @@ public class TaskList extends Activity { @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { + Intent intent; + TaskModelForList task; + switch(item.getItemId()) { case INSERT_ID: createTask(); @@ -333,12 +391,17 @@ public class TaskList extends Activity { listView.showContextMenu(); return true; case TAGS_ID: - // TODO + if(filterTag == null) { + intent = new Intent(TaskList.this, TagList.class); + startActivityForResult(intent, ACTIVITY_TAGS); + } else { + finish(); + } return true; case CONTEXT_EDIT_ID: - TaskModelForList task = taskArray.get(item.getGroupId()); - Intent intent = new Intent(TaskList.this, TaskEdit.class); + task = taskArray.get(item.getGroupId()); + intent = new Intent(TaskList.this, TaskEdit.class); intent.putExtra(TaskEdit.LOAD_INSTANCE_TOKEN, task.getTaskIdentifier().getId()); startActivityForResult(intent, ACTIVITY_EDIT); return true; @@ -396,10 +459,10 @@ public class TaskList extends Activity { item.setIcon(android.R.drawable.ic_menu_view); item.setAlphabeticShortcut('f'); - /*item = menu.add(Menu.NONE, TAGS_ID, Menu.NONE, + item = menu.add(Menu.NONE, TAGS_ID, Menu.NONE, R.string.taskList_menu_tags); item.setIcon(android.R.drawable.ic_menu_myplaces); - item.setAlphabeticShortcut('t');*/ + item.setAlphabeticShortcut('t'); /*item = menu.add(Menu.NONE, SETTINGS_ID, Menu.NONE, R.string.taskList_menu_settings); @@ -410,8 +473,10 @@ public class TaskList extends Activity { } @Override - protected void onStop() { - super.onStop(); + protected void onDestroy() { + super.onDestroy(); controller.close(); + if(tagController != null) + tagController.close(); } } \ No newline at end of file diff --git a/src/com/timsu/astrid/activities/TaskModificationActivity.java b/src/com/timsu/astrid/activities/TaskModificationActivity.java index 5999049cf..3d8025f5c 100644 --- a/src/com/timsu/astrid/activities/TaskModificationActivity.java +++ b/src/com/timsu/astrid/activities/TaskModificationActivity.java @@ -1,3 +1,22 @@ +/* + * ASTRID: Android's Simple Task Recording Dashboard + * + * Copyright (c) 2009 Tim Su + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ package com.timsu.astrid.activities; import android.app.Activity; @@ -25,7 +44,7 @@ public abstract class TaskModificationActivity { }); } }; - updateTimer = new Timer(); } private void setUpListeners() { @@ -237,7 +236,8 @@ public class TaskView extends TaskModificationActivity { protected void onResume() { super.onResume(); populateFields(); - updateTimer.scheduleAtFixedRate(updateTimerTask, 0, 1000); // start timer + updateTimer = new Timer(); // start timer + updateTimer.scheduleAtFixedRate(updateTimerTask, 0, 1000); } // --- event response methods diff --git a/src/com/timsu/astrid/data/AbstractController.java b/src/com/timsu/astrid/data/AbstractController.java index dc5d2177a..8663f7e47 100644 --- a/src/com/timsu/astrid/data/AbstractController.java +++ b/src/com/timsu/astrid/data/AbstractController.java @@ -19,20 +19,73 @@ */ package com.timsu.astrid.data; -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; +import java.lang.reflect.InvocationTargetException; +import java.util.Iterator; + +import android.app.Activity; +import android.database.Cursor; +import android.util.Log; abstract public class AbstractController { - protected Context context; - protected SQLiteDatabase database; + protected Activity activity; // special columns public static final String KEY_ROWID = "_id"; - // database names + // database and table names protected static final String TASK_TABLE_NAME = "tasks"; + protected static final String TAG_TABLE_NAME = "tags"; + protected static final String TAG_TASK_MAP_NAME = "tagTaskMap"; + + // cursor iterator + + public static class CursorIterator implements Iterator { + Cursor cursor; + Class cls; + + public CursorIterator(Cursor cursor, Class cls) { + this.cursor = cursor; + this.cls = cls; + } + + @Override + public boolean hasNext() { + return !cursor.isLast(); + } + + @Override + public TYPE next() { + try { + TYPE model = cls.getConstructor(Cursor.class).newInstance(cursor); + cursor.moveToNext(); + return model; + + // ugh... + } catch (IllegalArgumentException e) { + Log.e("CursorIterator", e.toString()); + } catch (SecurityException e) { + Log.e("CursorIterator", e.toString()); + } catch (InstantiationException e) { + Log.e("CursorIterator", e.toString()); + } catch (IllegalAccessException e) { + Log.e("CursorIterator", e.toString()); + } catch (InvocationTargetException e) { + Log.e("CursorIterator", e.toString()); + } catch (NoSuchMethodException e) { + Log.e("CursorIterator", e.toString()); + } + + return null; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Can't remove this way"); + } + + } } diff --git a/src/com/timsu/astrid/data/Identifier.java b/src/com/timsu/astrid/data/Identifier.java new file mode 100644 index 000000000..17c8805db --- /dev/null +++ b/src/com/timsu/astrid/data/Identifier.java @@ -0,0 +1,31 @@ +package com.timsu.astrid.data; + +/** Identifier of a single object. Extend this class to create your own */ +public abstract class Identifier { + private long id; + + public Identifier(long id) { + this.id = id; + } + + public long getId() { + return id; + } + + public String idAsString() { + return Long.toString(id); + } + + @Override + public int hashCode() { + return (int)id; + } + + @Override + public boolean equals(Object o) { + if(o == null || o.getClass() != getClass()) + return false; + + return ((Identifier)o).getId() == getId(); + } +} diff --git a/src/com/timsu/astrid/data/tag/AbstractTagModel.java b/src/com/timsu/astrid/data/tag/AbstractTagModel.java new file mode 100644 index 000000000..75ae137ab --- /dev/null +++ b/src/com/timsu/astrid/data/tag/AbstractTagModel.java @@ -0,0 +1,177 @@ +package com.timsu.astrid.data.tag; + +import java.util.Date; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import com.timsu.astrid.data.AbstractController; +import com.timsu.astrid.data.AbstractModel; + + +/** Abstract model of a task. Subclasses implement the getters and setters + * they are interested in. + * + * @author timsu + * + */ +public abstract class AbstractTagModel extends AbstractModel { + + /** Version number of this model */ + static final int VERSION = 1; + + // field names + + static final String NAME = "name"; + static final String NOTES = "notes"; + // reserved fields + static final String ICON = "icon"; + static final String PARENT = "parent"; + static final String FLAGS = "flags"; + static final String LOCATION_LAT = "locationLat"; + static final String LOCATION_LONG = "locationLong"; + static final String NOTIFICATIONS = "notifications"; + // end reserved fields + static final String CREATION_DATE = "creationDate"; + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + static { + defaultValues.put(NAME, ""); + defaultValues.put(NOTES, ""); + defaultValues.put(ICON, (Integer)null); + defaultValues.put(PARENT, (Long)null); + defaultValues.put(FLAGS, (Integer)0); + defaultValues.put(LOCATION_LAT, (Integer)null); + defaultValues.put(LOCATION_LONG, (Integer)null); + defaultValues.put(NOTIFICATIONS, 0); + } + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + // --- database helper + + /** Database Helper manages creating new tables and updating old ones */ + static class TagModelDatabaseHelper extends SQLiteOpenHelper { + String tableName; + + TagModelDatabaseHelper(Context context, String databaseName, String tableName) { + super(context, databaseName, null, VERSION); + this.tableName = tableName; + } + + @Override + public void onCreate(SQLiteDatabase db) { + String sql = new StringBuilder(). + append("CREATE TABLE ").append(tableName).append(" ("). + append(AbstractController.KEY_ROWID).append(" integer primary key autoincrement, "). + append(NAME).append(" text unique not null,"). + append(NOTES).append(" text not null,"). + append(ICON).append(" integer,"). + append(PARENT).append(" integer,"). + append(FLAGS).append(" integer not null,"). + append(LOCATION_LAT).append(" integer,"). + append(LOCATION_LONG).append(" integer,"). + append(NOTIFICATIONS).append(" integer,"). + append(CREATION_DATE).append(" integer"). + append(");").toString(); + db.execSQL(sql); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(getClass().getSimpleName(), "Upgrading database from version " + + oldVersion + " to " + newVersion + "."); + + switch(oldVersion) { + default: + // we don't know how to handle it... do the unfortunate thing + Log.e(getClass().getSimpleName(), "Unsupported migration, table dropped!"); + db.execSQL("DROP TABLE IF EXISTS " + tableName); + onCreate(db); + } + } + } + + // --- utility methods + + + + // --- identifier + + private TagIdentifier identifier = null; + + public TagIdentifier getTagIdentifier() { + return identifier; + } + + void setTagIdentifier(TagIdentifier identifier) { + this.identifier = identifier; + } + + // --- constructor pass-through + + AbstractTagModel() { + super(); + } + + /** Read identifier from database */ + AbstractTagModel(Cursor cursor) { + super(cursor); + + Integer id = retrieveInteger(AbstractController.KEY_ROWID); + setTagIdentifier(new TagIdentifier(id)); + } + + /** Get identifier from argument */ + AbstractTagModel(TagIdentifier identifier, Cursor cursor) { + super(cursor); + + setTagIdentifier(identifier); + } + + // --- getters and setters: expose them as you see fit + + protected String getName() { + return retrieveString(NAME); + } + + protected String getNotes() { + return retrieveString(NOTES); + } + + protected Date getCreationDate() { + return retrieveDate(CREATION_DATE); + } + + // --- setters + + protected void setName(String name) { + setValues.put(NAME, name); + } + + protected void setNotes(String notes) { + setValues.put(NOTES, notes); + } + + protected void setCreationDate(Date creationDate) { + putDate(setValues, CREATION_DATE, creationDate); + } + + // --- utility methods + + static void putDate(ContentValues cv, String fieldName, Date date) { + if(date == null) + cv.put(fieldName, (Long)null); + else + cv.put(fieldName, date.getTime()); + } +} diff --git a/src/com/timsu/astrid/data/tag/TagController.java b/src/com/timsu/astrid/data/tag/TagController.java new file mode 100644 index 000000000..fd9cc181c --- /dev/null +++ b/src/com/timsu/astrid/data/tag/TagController.java @@ -0,0 +1,202 @@ +package com.timsu.astrid.data.tag; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import android.app.Activity; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; + +import com.timsu.astrid.data.AbstractController; +import com.timsu.astrid.data.tag.AbstractTagModel.TagModelDatabaseHelper; +import com.timsu.astrid.data.tag.TagToTaskMapping.TagToTaskMappingDatabaseHelper; +import com.timsu.astrid.data.task.TaskIdentifier; + +public class TagController extends AbstractController { + + private SQLiteDatabase tagDatabase, tagToTaskMapDatabase; + + // --- tag batch operations + + /** Get a list of all tags */ + public List getAllTags() + throws SQLException { + List list = new LinkedList(); + Cursor cursor = tagDatabase.query(TAG_TABLE_NAME, + TagModelForView.FIELD_LIST, null, null, null, null, null, null); + activity.startManagingCursor(cursor); + + if(cursor.getCount() == 0) + return list; + do { + cursor.moveToNext(); + list.add(new TagModelForView(cursor)); + } while(!cursor.isLast()); + + return list; + } + + // --- tag to task map batch operations + + /** Get a list of all tags as an id => tag map */ + public Map getAllTagsAsMap() throws SQLException { + Map map = new HashMap(); + for(TagModelForView tag : getAllTags()) + map.put(tag.getTagIdentifier(), tag); + return map; + } + + /** Get a list of tag identifiers for the given task */ + public List getTaskTags(TaskIdentifier + taskId) throws SQLException { + List list = new LinkedList(); + Cursor cursor = tagToTaskMapDatabase.query(TAG_TASK_MAP_NAME, + TagToTaskMapping.FIELD_LIST, TagToTaskMapping.TASK + " = ?", + new String[] { taskId.idAsString() }, null, null, null); + activity.startManagingCursor(cursor); + + if(cursor.getCount() == 0) + return list; + do { + cursor.moveToNext(); + list.add(new TagToTaskMapping(cursor).getTag()); + } while(!cursor.isLast()); + + return list; + } + + /** Get a list of task identifiers for the given tag */ + public List getTaggedTasks(TagIdentifier + tagId) throws SQLException { + List list = new LinkedList(); + Cursor cursor = tagToTaskMapDatabase.query(TAG_TASK_MAP_NAME, + TagToTaskMapping.FIELD_LIST, TagToTaskMapping.TAG + " = ?", + new String[] { tagId.idAsString() }, null, null, null); + activity.startManagingCursor(cursor); + + if(cursor.getCount() == 0) + return list; + do { + cursor.moveToNext(); + list.add(new TagToTaskMapping(cursor).getTask()); + } while(!cursor.isLast()); + + return list; + } + + // --- single tag operations + + public TagIdentifier createTag(String name) throws SQLException { + if(name == null) + throw new NullPointerException("Name can't be null"); + + TagModelForView newTag = new TagModelForView(name); + long row = tagDatabase.insertOrThrow(TAG_TABLE_NAME, AbstractTagModel.NAME, + newTag.getMergedValues()); + return new TagIdentifier(row); + } + + /** Creates or saves the given tag */ + public boolean saveTag(AbstractTagModel tag) throws SQLException { + boolean saveSucessful; + + if(tag.getTagIdentifier() == null) { + long newRow = tagDatabase.insert(TAG_TABLE_NAME, AbstractTagModel.NAME, + tag.getMergedValues()); + tag.setTagIdentifier(new TagIdentifier(newRow)); + + saveSucessful = newRow >= 0; + } else { + long id = tag.getTagIdentifier().getId(); + saveSucessful = tagDatabase.update(TAG_TABLE_NAME, tag.getSetValues(), + KEY_ROWID + "=" + id, null) > 0; + } + + return saveSucessful; + } + + /** Returns a TaskModelForView corresponding to the given TaskIdentifier */ + public TagModelForView fetchTagForView(TagIdentifier tagId) throws SQLException { + long id = tagId.getId(); + Cursor cursor = tagDatabase.query(true, TAG_TABLE_NAME, + TagModelForView.FIELD_LIST, + KEY_ROWID + "=" + id, null, null, null, null, null); + + if (cursor != null) { + cursor.moveToFirst(); + TagModelForView model = new TagModelForView(cursor); + return model; + } + + throw new SQLException("Returned empty set!"); + } + + /** Deletes the tag and removes tag/task mappings */ + public boolean deleteTag( TagIdentifier tagId) + throws SQLException{ + if(tagToTaskMapDatabase.delete(TAG_TASK_MAP_NAME, + TagToTaskMapping.TAG + " = " + tagId.idAsString(), null) < 0) + return false; + + return tagDatabase.delete(TAG_TABLE_NAME, + KEY_ROWID + " = " + tagId.toString(), null) > 0; + } + + // --- single tag to task operations + + /** Remove the given tag from the task */ + public boolean removeTag(TaskIdentifier taskId, TagIdentifier tagId) + throws SQLException{ + return tagToTaskMapDatabase.delete(TAG_TASK_MAP_NAME, + String.format("%s = ? AND %s = ?", + TagToTaskMapping.TAG, TagToTaskMapping.TASK), + new String[] { tagId.idAsString(), taskId.idAsString() }) > 0; + } + + /** Add the given tag to the task */ + public boolean addTag(TaskIdentifier taskId, TagIdentifier tagId) + throws SQLException { + ContentValues values = new ContentValues(); + values.put(TagToTaskMapping.TAG, tagId.getId()); + values.put(TagToTaskMapping.TASK, taskId.getId()); + return tagToTaskMapDatabase.insert(TAG_TASK_MAP_NAME, TagToTaskMapping.TAG, + values) >= 0; + } + + // --- boilerplate + + /** + * Constructor - takes the context to allow the database to be + * opened/created + */ + public TagController(Activity activity) { + this.activity = activity; + } + + /** + * Open the notes database. If it cannot be opened, try to create a new + * instance of the database. If it cannot be created, throw an exception to + * signal the failure + * + * @return this (self reference, allowing this to be chained in an + * initialization call) + * @throws SQLException if the database could be neither opened or created + */ + public TagController open() throws SQLException { + tagToTaskMapDatabase = new TagToTaskMappingDatabaseHelper(activity, + TAG_TASK_MAP_NAME, TAG_TASK_MAP_NAME).getWritableDatabase(); + tagDatabase = new TagModelDatabaseHelper(activity, + TAG_TABLE_NAME, TAG_TABLE_NAME).getWritableDatabase(); + return this; + } + + /** Closes database resource */ + public void close() { + tagDatabase.close(); + tagToTaskMapDatabase.close(); + } +} diff --git a/src/com/timsu/astrid/data/tag/TagIdentifier.java b/src/com/timsu/astrid/data/tag/TagIdentifier.java new file mode 100644 index 000000000..7a6f24b51 --- /dev/null +++ b/src/com/timsu/astrid/data/tag/TagIdentifier.java @@ -0,0 +1,12 @@ +package com.timsu.astrid.data.tag; + +import com.timsu.astrid.data.Identifier; + + +public class TagIdentifier extends Identifier { + + public TagIdentifier(long id) { + super(id); + } + +} diff --git a/src/com/timsu/astrid/data/tag/TagModelForView.java b/src/com/timsu/astrid/data/tag/TagModelForView.java new file mode 100644 index 000000000..65b6158a6 --- /dev/null +++ b/src/com/timsu/astrid/data/tag/TagModelForView.java @@ -0,0 +1,42 @@ +package com.timsu.astrid.data.tag; + +import android.database.Cursor; + +import com.timsu.astrid.data.AbstractController; + + + +/** Fields that you would want to see in the TaskView activity */ +public class TagModelForView extends AbstractTagModel { + + static String[] FIELD_LIST = new String[] { + AbstractController.KEY_ROWID, + NAME, + }; + + // --- constructors + + /** Constructor for creating a new model */ + TagModelForView(String name) { + super(); + setName(name); + } + + /** Constructor for getting an existing model */ + TagModelForView(Cursor cursor) { + super(cursor); + getName(); + } + + // --- getters and setters + + @Override + public String getName() { + return super.getName(); + } + + @Override + public String toString() { + return getName(); + } +} diff --git a/src/com/timsu/astrid/data/tag/TagToTaskMapping.java b/src/com/timsu/astrid/data/tag/TagToTaskMapping.java new file mode 100644 index 000000000..867c7c529 --- /dev/null +++ b/src/com/timsu/astrid/data/tag/TagToTaskMapping.java @@ -0,0 +1,116 @@ +package com.timsu.astrid.data.tag; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import com.timsu.astrid.data.AbstractController; +import com.timsu.astrid.data.AbstractModel; +import com.timsu.astrid.data.task.TaskIdentifier; + + +/** A single tag on a task + * + * @author timsu + * + */ +public class TagToTaskMapping extends AbstractModel { + + /** Version number of this model */ + static final int VERSION = 2; + + // field names + + static final String TASK = "task"; + static final String TAG = "tag"; + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + static String[] FIELD_LIST = new String[] { + AbstractController.KEY_ROWID, + TASK, + TAG, + }; + + // --- database helper + + /** Database Helper manages creating new tables and updating old ones */ + static class TagToTaskMappingDatabaseHelper extends SQLiteOpenHelper { + String tableName; + + TagToTaskMappingDatabaseHelper(Context context, String databaseName, String tableName) { + super(context, databaseName, null, VERSION); + this.tableName = tableName; + } + + @Override + public void onCreate(SQLiteDatabase db) { + String sql = new StringBuilder(). + append("CREATE TABLE ").append(tableName).append(" ("). + append(AbstractController.KEY_ROWID).append(" integer primary key autoincrement, "). + append(TASK).append(" integer not null,"). + append(TAG).append(" integer not null,"). + append("unique (").append(TASK).append(",").append(TAG).append(")"). + append(");").toString(); + db.execSQL(sql); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(getClass().getSimpleName(), "Upgrading database from version " + + oldVersion + " to " + newVersion + "."); + + switch(oldVersion) { + default: + // we don't know how to handle it... do the unfortunate thing + Log.e(getClass().getSimpleName(), "Unsupported migration, table dropped!"); + db.execSQL("DROP TABLE IF EXISTS " + tableName); + onCreate(db); + } + } + } + + + // --- constructor pass-through + + TagToTaskMapping(TaskIdentifier task, TagIdentifier tag) { + super(); + setTask(task); + setTag(tag); + } + + TagToTaskMapping(Cursor cursor) { + super(cursor); + } + + // --- getters and setters: expose them as you see fit + + public boolean isNew() { + return getCursor() == null; + } + + public TaskIdentifier getTask() { + return new TaskIdentifier(retrieveInteger(TASK)); + } + + public TagIdentifier getTag() { + return new TagIdentifier(retrieveInteger(TAG)); + } + + private void setTask(TaskIdentifier task) { + setValues.put(TASK, task.getId()); + } + + private void setTag(TagIdentifier tag) { + setValues.put(TAG, tag.getId()); + } +} diff --git a/src/com/timsu/astrid/data/task/AbstractTaskModel.java b/src/com/timsu/astrid/data/task/AbstractTaskModel.java index 3ae274d16..8b0d5fd6a 100644 --- a/src/com/timsu/astrid/data/task/AbstractTaskModel.java +++ b/src/com/timsu/astrid/data/task/AbstractTaskModel.java @@ -40,8 +40,10 @@ public abstract class AbstractTaskModel extends AbstractModel { static final String DEFINITE_DUE_DATE = "definiteDueDate"; static final String PREFERRED_DUE_DATE = "preferredDueDate"; static final String HIDDEN_UNTIL = "hiddenUntil"; + // reserved fields static final String BLOCKING_ON = "blockingOn"; static final String NOTIFICATIONS = "notifications"; + // end reserved fields static final String CREATION_DATE = "creationDate"; static final String COMPLETION_DATE = "completionDate"; @@ -75,10 +77,10 @@ public abstract class AbstractTaskModel extends AbstractModel { static class TaskModelDatabaseHelper extends SQLiteOpenHelper { String tableName; - TaskModelDatabaseHelper(Context context, String databaseName) { + TaskModelDatabaseHelper(Context context, String databaseName, String tableName) { super(context, databaseName, null, VERSION); - this.tableName = databaseName; + this.tableName = tableName; } @Override diff --git a/src/com/timsu/astrid/data/task/TaskController.java b/src/com/timsu/astrid/data/task/TaskController.java index d8ce91ae7..b9dfbf9b9 100644 --- a/src/com/timsu/astrid/data/task/TaskController.java +++ b/src/com/timsu/astrid/data/task/TaskController.java @@ -1,10 +1,12 @@ package com.timsu.astrid.data.task; +import java.util.ArrayList; import java.util.List; -import android.content.Context; +import android.app.Activity; import android.database.Cursor; import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import com.timsu.astrid.data.AbstractController; @@ -12,6 +14,8 @@ import com.timsu.astrid.data.task.AbstractTaskModel.TaskModelDatabaseHelper; public class TaskController extends AbstractController { + private SQLiteDatabase database; + // --- task list operations /** Return a list of all of the tasks with progress < COMPLETE_PERCENTAGE */ @@ -31,7 +35,38 @@ public class TaskController extends AbstractController { /** Create a weighted list of tasks from the db cursor given */ public List createTaskListFromCursor(Cursor cursor, boolean hideHidden) { - return TaskModelForList.createTaskModelList(cursor, hideHidden); + List list = new ArrayList(); + + if(cursor.getCount() == 0) + return list; + + do { + cursor.moveToNext(); + list.add(new TaskModelForList(cursor)); + } while(!cursor.isLast()); + + return TaskModelForList.sortAndFilterList(list, hideHidden); + } + + /** Create a weighted list of tasks from the db cursor given */ + public Cursor getTaskListCursorById(List idList) { + + StringBuilder where = new StringBuilder(); + for(int i = 0; i < idList.size(); i++) { + where.append(KEY_ROWID); + where.append("="); + where.append(idList.get(i).toString()); + if(i < idList.size()-1) + where.append(" OR "); + } + + // hack for empty arrays + if(idList.size() == 0) + where.append("0"); + + return database.query(true, TASK_TABLE_NAME, + TaskModelForList.FIELD_LIST, where.toString(), null, null, + null, null, null); } // --- single task operations @@ -74,12 +109,13 @@ public class TaskController extends AbstractController { } /** Returns a TaskModelForEdit corresponding to the given TaskIdentifier */ - public TaskModelForEdit fetchTaskForEdit(TaskIdentifier taskId) throws SQLException { + public TaskModelForEdit fetchTaskForEdit(TaskIdentifier + taskId) throws SQLException { long id = taskId.getId(); Cursor cursor = database.query(true, TASK_TABLE_NAME, TaskModelForEdit.FIELD_LIST, KEY_ROWID + "=" + id, null, null, null, null, null); - + activity.startManagingCursor(cursor); if (cursor != null) { cursor.moveToFirst(); TaskModelForEdit model = new TaskModelForEdit(taskId, cursor); @@ -97,7 +133,7 @@ public class TaskController extends AbstractController { Cursor cursor = database.query(true, TASK_TABLE_NAME, TaskModelForView.FIELD_LIST, KEY_ROWID + "=" + id, null, null, null, null, null); - + activity.startManagingCursor(cursor); if (cursor != null) { cursor.moveToFirst(); TaskModelForView model = new TaskModelForView(taskId, cursor); @@ -114,8 +150,8 @@ public class TaskController extends AbstractController { * Constructor - takes the context to allow the database to be * opened/created */ - public TaskController(Context context) { - this.context = context; + public TaskController(Activity activity) { + this.activity = activity; } /** @@ -129,7 +165,7 @@ public class TaskController extends AbstractController { */ public TaskController open() throws SQLException { SQLiteOpenHelper databaseHelper = new TaskModelDatabaseHelper( - context, TASK_TABLE_NAME); + activity, TASK_TABLE_NAME, TASK_TABLE_NAME); database = databaseHelper.getWritableDatabase(); return this; } diff --git a/src/com/timsu/astrid/data/task/TaskIdentifier.java b/src/com/timsu/astrid/data/task/TaskIdentifier.java index 5d1135789..a1c3c0741 100644 --- a/src/com/timsu/astrid/data/task/TaskIdentifier.java +++ b/src/com/timsu/astrid/data/task/TaskIdentifier.java @@ -1,15 +1,11 @@ package com.timsu.astrid.data.task; +import com.timsu.astrid.data.Identifier; -/** A little class that identifies a task. For saving state and passing around */ -public class TaskIdentifier { - private long id; - public TaskIdentifier(long id) { - this.id = id; - } +public class TaskIdentifier extends Identifier { - public long getId() { - return id; + public TaskIdentifier(long id) { + super(id); } } diff --git a/src/com/timsu/astrid/data/task/TaskModelForList.java b/src/com/timsu/astrid/data/task/TaskModelForList.java index ece2886c6..650646f84 100644 --- a/src/com/timsu/astrid/data/task/TaskModelForList.java +++ b/src/com/timsu/astrid/data/task/TaskModelForList.java @@ -1,10 +1,10 @@ package com.timsu.astrid.data.task; -import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import android.database.Cursor; @@ -30,25 +30,24 @@ public class TaskModelForList extends AbstractTaskModel { HIDDEN_UNTIL, }; - static List createTaskModelList(Cursor cursor, - boolean hideHidden) { - ArrayList list = new ArrayList(); + /** Takes the incoming list of task models and weights it, removing hidden + * tasks if desired. This mutates the list */ + static List sortAndFilterList( + List list, boolean hideHidden) { final HashMap weights = new HashMap(); // first, load everything - for(int i = 0; i < cursor.getCount(); i++) { - cursor.moveToNext(); - TaskModelForList task = new TaskModelForList(cursor); - - // hide tasks + for(Iterator i = list.iterator(); i.hasNext(); ) { + TaskModelForList task = i.next(); if(hideHidden) { - if(task.getHiddenUntil() != null && - task.getHiddenUntil().getTime() > System.currentTimeMillis()) - continue; + if(task.getHiddenUntil() != null && task.getHiddenUntil(). + getTime() > System.currentTimeMillis()) { + i.remove(); + continue; + } } - list.add(task); weights.put(task, task.getWeight()); } diff --git a/src/com/timsu/astrid/widget/DateControlSet.java b/src/com/timsu/astrid/widget/DateControlSet.java new file mode 100644 index 000000000..179a53fac --- /dev/null +++ b/src/com/timsu/astrid/widget/DateControlSet.java @@ -0,0 +1,120 @@ +/* + * ASTRID: Android's Simple Task Recording Dame + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.timsu.astrid.widget; + +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Date; + +import android.app.Activity; +import android.app.DatePickerDialog; +import android.app.TimePickerDialog; +import android.app.DatePickerDialog.OnDateSetListener; +import android.app.TimePickerDialog.OnTimeSetListener; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.DatePicker; +import android.widget.TimePicker; +import android.widget.CompoundButton.OnCheckedChangeListener; + +public class DateControlSet implements OnTimeSetListener, + OnDateSetListener, View.OnClickListener { + + private static final Format dateFormatter = new SimpleDateFormat("EEE, MMM d, yyyy"); + private static final Format timeFormatter = new SimpleDateFormat("h:mm a"); + + private final Activity activity; + private CheckBox activatedCheckBox; + private Button dateButton; + private Button timeButton; + private Date date; + + public DateControlSet(Activity activity, int checkBoxId, int dateButtonId, int timeButtonId) { + this.activity = activity; + activatedCheckBox = (CheckBox)activity.findViewById(checkBoxId); + dateButton = (Button)activity.findViewById(dateButtonId); + timeButton = (Button)activity.findViewById(timeButtonId); + + activatedCheckBox.setOnCheckedChangeListener( + new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + dateButton.setEnabled(isChecked); + timeButton.setEnabled(isChecked); + } + }); + dateButton.setOnClickListener(this); + timeButton.setOnClickListener(this); + } + + public Date getDate() { + if(!activatedCheckBox.isChecked()) + return null; + return date; + } + + /** Initialize the components for the given date field */ + public void setDate(Date newDate) { + this.date = newDate; + if(newDate == null) { + date = new Date(); + date.setMinutes(0); + } + + activatedCheckBox.setChecked(newDate != null); + dateButton.setEnabled(newDate != null); + timeButton.setEnabled(newDate != null); + + updateDate(); + updateTime(); + } + + public void onDateSet(DatePicker view, int year, int month, int monthDay) { + date.setYear(year - 1900); + date.setMonth(month); + date.setDate(monthDay); + updateDate(); + } + + public void onTimeSet(TimePicker view, int hourOfDay, int minute) { + date.setHours(hourOfDay); + date.setMinutes(minute); + updateTime(); + } + + public void updateDate() { + dateButton.setText(dateFormatter.format(date)); + + } + + public void updateTime() { + timeButton.setText(timeFormatter.format(date)); + } + + public void onClick(View v) { + if(v == timeButton) + new TimePickerDialog(activity, this, date.getHours(), + date.getMinutes(), false).show(); + else + new DatePickerDialog(activity, this, 1900 + + date.getYear(), date.getMonth(), date.getDate()).show(); + } +} \ No newline at end of file diff --git a/src/com/timsu/astrid/widget/TimeDurationControlSet.java b/src/com/timsu/astrid/widget/TimeDurationControlSet.java new file mode 100644 index 000000000..d33a7723c --- /dev/null +++ b/src/com/timsu/astrid/widget/TimeDurationControlSet.java @@ -0,0 +1,79 @@ +/* + * ASTRID: Android's Simple Task Recording Dame + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.timsu.astrid.widget; + +import android.app.Activity; +import android.content.res.Resources; +import android.view.View; +import android.widget.Button; + +import com.timsu.astrid.R; +import com.timsu.astrid.utilities.DateUtilities; +import com.timsu.astrid.widget.NumberPickerDialog.OnNumberPickedListener; + +public class TimeDurationControlSet implements OnNumberPickedListener, + View.OnClickListener { + + private final Activity activity; + private Button timeButton; + private int timeDuration; + private final NumberPickerDialog dialog; + + public TimeDurationControlSet(Activity activity, int timeButtonId) { + this.activity = activity; + timeButton = (Button)activity.findViewById(timeButtonId); + timeButton.setOnClickListener(this); + dialog = new NumberPickerDialog(activity, this, + activity.getResources().getString(R.string.minutes_dialog), + 0, 5, 0, 999); + } + + public int getTimeDurationInSeconds() { + return timeDuration; + } + + public void setTimeElapsed(Integer timeDurationInSeconds) { + if(timeDurationInSeconds == null) + timeDurationInSeconds = 0; + + timeDuration = timeDurationInSeconds; + + Resources r = activity.getResources(); + if(timeDurationInSeconds == 0) { + timeButton.setText(r.getString(R.string.blank_button_title)); + return; + } + + timeButton.setText(DateUtilities.getDurationString(r, + timeDurationInSeconds, 2)); + dialog.setInitialValue(timeDuration/60); + } + + @Override + /** Called when NumberPicker activity is completed */ + public void onNumberPicked(NumberPicker view, int value) { + setTimeElapsed(value * 60); + } + + /** Called when time button is clicked */ + public void onClick(View v) { + dialog.show(); + } + + +} \ No newline at end of file