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.

pull/14/head
Tim Su 16 years ago
parent 0af4cf9d99
commit 065fc1a9d4

@ -17,5 +17,7 @@
<activity android:name=".activities.TaskEdit"/>
<activity android:name=".activities.TagList"/>
</application>
</manifest>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<AutoCompleteTextView android:id="@+id/text1"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"/>
<ImageButton android:id="@+id/button1"
style="?android:attr/buttonStyleInset"
android:src="@android:drawable/ic_delete"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginTop="2dip"
android:layout_marginRight="2dip"
android:layout_marginBottom="2dip"
android:gravity="center_vertical"
/>
</LinearLayout>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/taglist_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ListView android:id="@+id/taglist"
android:layout_weight="1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

@ -82,7 +82,7 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<!-- <TextView android:id="@+id/tags_label"
<TextView android:id="@+id/tags_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tags_label"
@ -92,7 +92,7 @@
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
</LinearLayout> -->
</LinearLayout>
</LinearLayout>
<!-- DATES -->

@ -9,6 +9,7 @@
android:layout_height="wrap_content">
<CheckBox android:id="@+id/cb1"
android:gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="5dip"/>
@ -18,13 +19,21 @@
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:paddingLeft="5dip"/>
<TextView android:id="@+id/text1"
android:layout_height="?android:attr/listPreferredItemHeight"
android:layout_width="fill_parent"
android:layout_width="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center_vertical"
android:paddingLeft="5dip"
android:singleLine="true"/>
<TextView android:id="@+id/text2"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="center_vertical"
android:paddingLeft="15dip"
android:singleLine="true"/>
</LinearLayout>

@ -17,6 +17,10 @@
<item quantity="one">1 Task</item>
<item quantity="other">%d Tasks</item>
</plurals>
<plurals name="Ntags">
<item quantity="one">1 Tag</item>
<item quantity="other">%d Tags</item>
</plurals>
<!-- Time Constants -->
<plurals name="Ndays">
<item quantity="one">1 Day</item>
@ -38,6 +42,7 @@
<!-- TaskList -->
<skip />
<string name="taskList_titlePrefix">Astrid: </string>
<string name="taskList_titleTagPrefix">Tasks Tagged \"%s\": </string>
<string name="taskList_hiddenSuffix"> hidden</string>
<string name="addtask_label">New Task</string>
@ -96,10 +101,18 @@
<string name="taskView_notes">Task Notes</string>
<string name="overdue_suffix"> Overdue</string>
<string name="progress_dialog">% of Task Finished</string>
<!-- TaskList -->
<skip />
<string name="tagList_titlePrefix">Astrid: Tag View: </string>
<string name="tagList_context_create">Create Task With Tag</string>
<string name="tagList_context_edit">Edit Tag</string>
<string name="tagList_context_delete">Delete Tag</string>
<!-- Utilities -->
<skip />
<string name="delete_title">Delete</string>
<string name="delete_this_task_title">Delete this task?</string>
<string name="delete_this_tag_title">Remove this tag from all tasks?</string>
</resources>

@ -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<TagModelForView> 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<TagModelForView> {
private List<TagModelForView> objects;
private int resource;
private LayoutInflater inflater;
public TagListAdapter(Context context, int resource,
List<TagModelForView> 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<TaskIdentifier> 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();
}
}

@ -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<TaskModelForEdit> {
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<TaskModelForEdit> {
private DateControlSet preferredDueDate;
private DateControlSet hiddenUntil;
private EditText notes;
private LinearLayout tagsContainer;
// other instance variables
private boolean shouldSaveState = true;
private TagController tagController;
private List<TagModelForView> tags;
private List<TagIdentifier> 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<TaskModelForEdit> {
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<TagIdentifier, TagModelForView> tagsMap =
new HashMap<TagIdentifier, TagModelForView>();
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<TagIdentifier>();
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<TagIdentifier> tagsToDelete;
Set<TagIdentifier> tagsToAdd;
HashSet<String> tagNames = new HashSet<String>();
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<TagIdentifier> tagIds = new HashSet<TagIdentifier>();
HashMap<String, TagIdentifier> tagsByName = new HashMap<String, TagIdentifier>();
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<TagIdentifier>(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<TaskModelForEdit> {
importance.setAdapter(importanceAdapter);
}
/** Display importance with proper formatting */
private class ImportanceAdapter extends ArrayAdapter<Importance> {
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<TaskModelForEdit> {
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<TagModelForView> tagsAdapter =
new ArrayAdapter<TagModelForView>(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<TaskModelForEdit> {
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<Importance> {
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;
}
}
}

@ -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<TaskModelForList> taskArray;
private Map<TagIdentifier, TagModelForView> 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<TaskIdentifier> 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<TagIdentifier> tags = tagController.getTaskTags(
task.getTaskIdentifier());
StringBuilder tagString = new StringBuilder();
for(Iterator<TagIdentifier> 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();
}
}

@ -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<MODEL_TYPE extends
if(savedInstanceState != null && savedInstanceState.containsKey(LOAD_INSTANCE_TOKEN)) {
identifier = new TaskIdentifier(savedInstanceState.getLong(
LOAD_INSTANCE_TOKEN));
} else if(extras != null)
} else if(extras != null && extras.containsKey(LOAD_INSTANCE_TOKEN))
identifier = new TaskIdentifier(extras.getLong(
LOAD_INSTANCE_TOKEN));
@ -35,8 +54,8 @@ public abstract class TaskModificationActivity<MODEL_TYPE extends
abstract protected MODEL_TYPE getModel(TaskIdentifier identifier);
@Override
protected void onStop() {
super.onStop();
protected void onDestroy() {
super.onDestroy();
controller.close();
}

@ -1,5 +1,5 @@
/*
* ASTRID: Android's Simple Task Recording Dame
* ASTRID: Android's Simple Task Recording Dashboard
*
* Copyright (c) 2009 Tim Su
*
@ -124,7 +124,6 @@ public class TaskView extends TaskModificationActivity<TaskModelForView> {
});
}
};
updateTimer = new Timer();
}
private void setUpListeners() {
@ -237,7 +236,8 @@ public class TaskView extends TaskModificationActivity<TaskModelForView> {
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

@ -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<TYPE extends AbstractModel> implements Iterator<TYPE> {
Cursor cursor;
Class<TYPE> cls;
public CursorIterator(Cursor cursor, Class<TYPE> 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");
}
}
}

@ -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();
}
}

@ -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());
}
}

@ -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<TagModelForView> getAllTags()
throws SQLException {
List<TagModelForView> list = new LinkedList<TagModelForView>();
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<TagIdentifier, TagModelForView> getAllTagsAsMap() throws SQLException {
Map<TagIdentifier, TagModelForView> map = new HashMap<TagIdentifier, TagModelForView>();
for(TagModelForView tag : getAllTags())
map.put(tag.getTagIdentifier(), tag);
return map;
}
/** Get a list of tag identifiers for the given task */
public List<TagIdentifier> getTaskTags(TaskIdentifier
taskId) throws SQLException {
List<TagIdentifier> list = new LinkedList<TagIdentifier>();
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<TaskIdentifier> getTaggedTasks(TagIdentifier
tagId) throws SQLException {
List<TaskIdentifier> list = new LinkedList<TaskIdentifier>();
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();
}
}

@ -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);
}
}

@ -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();
}
}

@ -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());
}
}

@ -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

@ -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<TaskModelForList> createTaskListFromCursor(Cursor cursor,
boolean hideHidden) {
return TaskModelForList.createTaskModelList(cursor, hideHidden);
List<TaskModelForList> list = new ArrayList<TaskModelForList>();
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<TaskIdentifier> 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;
}

@ -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);
}
}

@ -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<TaskModelForList> createTaskModelList(Cursor cursor,
boolean hideHidden) {
ArrayList<TaskModelForList> list = new ArrayList<TaskModelForList>();
/** Takes the incoming list of task models and weights it, removing hidden
* tasks if desired. This mutates the list */
static List<TaskModelForList> sortAndFilterList(
List<TaskModelForList> list, boolean hideHidden) {
final HashMap<TaskModelForList, Integer> weights = new
HashMap<TaskModelForList, Integer>();
// first, load everything
for(int i = 0; i < cursor.getCount(); i++) {
cursor.moveToNext();
TaskModelForList task = new TaskModelForList(cursor);
// hide tasks
for(Iterator<TaskModelForList> 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());
}

@ -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();
}
}

@ -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();
}
}
Loading…
Cancel
Save