Got the awesome filter screen working. Won't save as bookmarks, but at least... it works and can filter tasks. Can edit the chaining type, can delete rows. Pretty much awesome.

pull/14/head
Tim Su 16 years ago
parent 0f77edb13e
commit 8e7b96f78f

@ -3,10 +3,15 @@
*/
package com.todoroo.astrid.api;
import java.util.Date;
import android.content.ContentValues;
import android.graphics.Bitmap;
import android.os.Parcel;
import android.os.Parcelable;
import com.todoroo.andlib.utility.DateUtilities;
import edu.umd.cs.findbugs.annotations.CheckForNull;
/**
@ -16,9 +21,9 @@ import edu.umd.cs.findbugs.annotations.CheckForNull;
* @author Tim Su <tim@todoroo.com>
*
*/
public final class CustomFilterCriterion extends FilterListItem {
public final class CustomFilterCriterion implements Parcelable {
// --- constants
// --- placeholder strings
/** value to be replaced with the current time as long */
public static final String VALUE_NOW = "NOW()"; //$NON-NLS-1$
@ -38,6 +43,28 @@ public final class CustomFilterCriterion extends FilterListItem {
/** value to be replaced by end of day next week as long */
public static final String VALUE_EOD_NEXT_WEEK = "EODW()"; //$NON-NLS-1$
/** Replace placeholder strings with actual */
public static String replacePlaceholders(String value) {
if(value.contains(VALUE_NOW))
value = value.replace(VALUE_NOW, Long.toString(DateUtilities.now()));
if(value.contains(VALUE_EOD) || value.contains(VALUE_EOD_DAY_AFTER) ||
value.contains(VALUE_EOD_NEXT_WEEK) || value.contains(VALUE_EOD_TOMORROW) ||
value.contains(VALUE_EOD_YESTERDAY)) {
Date date = new Date();
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
long time = date.getTime();
value = value.replace(VALUE_EOD_YESTERDAY, Long.toString(time - DateUtilities.ONE_DAY));
value = value.replace(VALUE_EOD, Long.toString(time));
value = value.replace(VALUE_EOD_TOMORROW, Long.toString(time + DateUtilities.ONE_DAY));
value = value.replace(VALUE_EOD_DAY_AFTER, Long.toString(time + 2 * DateUtilities.ONE_DAY));
value = value.replace(VALUE_EOD_NEXT_WEEK, Long.toString(time + 7 * DateUtilities.ONE_DAY));
}
return value;
}
// --- instance variables
/**
@ -131,9 +158,7 @@ public final class CustomFilterCriterion extends FilterListItem {
/**
* {@inheritDoc}
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(text);
dest.writeString(sql);
dest.writeParcelable(valuesForNewTasks, 0);
@ -169,4 +194,5 @@ public final class CustomFilterCriterion extends FilterListItem {
}
};
}

@ -9,8 +9,8 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.util.Log;
import com.todoroo.andlib.data.Property.PropertyVisitor;
@ -95,9 +95,12 @@ abstract public class AbstractDatabase {
}
protected synchronized final void initializeHelper() {
if(helper == null)
if(helper == null) {
if(ContextManager.getContext() == null)
throw new NullPointerException("Null context creating database helper");
helper = new DatabaseHelper(ContextManager.getContext(),
getName(), null, getVersion());
}
}
/**

@ -5,23 +5,31 @@ import java.util.List;
import android.app.ListActivity;
import android.content.ContentValues;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnCreateContextMenuListener;
import android.widget.Button;
import android.widget.TextView;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property.CountProperty;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.activity.TaskListActivity;
import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
@ -40,6 +48,8 @@ public class CustomFilterActivity extends ListActivity {
static final int MENU_GROUP_FILTER = 0;
static final int MENU_GROUP_FILTER_OPTION = 1;
static final int MENU_GROUP_CONTEXT_TYPE = 2;
static final int MENU_GROUP_CONTEXT_DELETE = 3;
// --- hierarchy of filter classes
@ -53,15 +63,16 @@ public class CustomFilterActivity extends ListActivity {
public CustomFilterCriterion criterion;
/** which of the entries is selected */
public int selectedIndex;
public int selectedIndex = -1;
/** type of join */
public int type;
public int type = TYPE_INTERSECT;
/** statistics for {@link FilterView} */
public int start, end, max;
}
private TextView filterName;
private CustomFilterAdapter adapter;
private final ArrayList<CustomFilterCriterion> criteria =
new ArrayList<CustomFilterCriterion>();
@ -74,13 +85,16 @@ public class CustomFilterActivity extends ListActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ContextManager.setContext(this);
setContentView(R.layout.custom_filter_activity);
setTitle(R.string.CFA_title);
DependencyInjectionService.getInstance().inject(this);
database.openForReading();
populateCriteria();
filterName = (TextView)findViewById(R.id.filterName);
List<CriterionInstance> startingCriteria = new ArrayList<CriterionInstance>();
startingCriteria.add(getStartingUniverse());
adapter = new CustomFilterAdapter(this, startingCriteria);
@ -109,7 +123,11 @@ public class CustomFilterActivity extends ListActivity {
values.put(Task.DUE_DATE.name, "%s");
CustomFilterCriterion criterion = new CustomFilterCriterion(
getString(R.string.CFC_dueBefore_text),
Query.select(Task.ID).from(Task.TABLE).where(Task.DUE_DATE.lte("%s")).toString(),
Query.select(Task.ID).from(Task.TABLE).where(
Criterion.and(
TaskCriteria.activeAndVisible(),
Task.DUE_DATE.gt(0),
Task.DUE_DATE.lte("%s"))).toString(),
values, r.getStringArray(R.array.CFC_dueBefore_entries),
entryValues, ((BitmapDrawable)r.getDrawable(R.drawable.tango_calendar)).getBitmap(),
getString(R.string.CFC_dueBefore_name));
@ -122,12 +140,17 @@ public class CustomFilterActivity extends ListActivity {
Integer.toString(Task.IMPORTANCE_SHOULD_DO),
Integer.toString(Task.IMPORTANCE_NONE),
};
String[] entries = new String[] {
"!!!!", "!!!", "!!", "!"
};
values = new ContentValues();
values.put(Task.IMPORTANCE.name, "%s");
criterion = new CustomFilterCriterion(
getString(R.string.CFC_importance_text),
Query.select(Task.ID).from(Task.TABLE).where(Task.IMPORTANCE.lte("%s")).toString(),
values, r.getStringArray(R.array.EPr_default_importance),
Query.select(Task.ID).from(Task.TABLE).where(
Criterion.and(TaskCriteria.activeAndVisible(),
Task.IMPORTANCE.lte("%s"))).toString(),
values, entries,
entryValues, ((BitmapDrawable)r.getDrawable(R.drawable.tango_warning)).getBitmap(),
getString(R.string.CFC_importance_name));
criteria.add(criterion);
@ -142,7 +165,9 @@ public class CustomFilterActivity extends ListActivity {
values.put(TagService.TAG.name, "%s");
criterion = new CustomFilterCriterion(
getString(R.string.CFC_tag_text),
Query.select(Metadata.TASK).from(Metadata.TABLE).where(Criterion.and(
Query.select(Metadata.TASK).from(Metadata.TABLE).join(Join.inner(
Task.TABLE, Metadata.TASK.eq(Task.ID))).where(Criterion.and(
TaskCriteria.activeAndVisible(),
MetadataCriteria.withKey(TagService.KEY),
TagService.TAG.eq("%s"))).toString(),
values, tagNames, tagNames,
@ -163,42 +188,155 @@ public class CustomFilterActivity extends ListActivity {
((Button)findViewById(R.id.add)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
menuItemInstance = null;
getListView().showContextMenu();
}
});
getListView().setOnCreateContextMenuListener(this);
((Button)findViewById(R.id.saveAndView)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveAndView();
}
});
getListView().setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
if(menu.hasVisibleItems())
return;
if(menuItemInstance == null) {
for(int i = 0; i < criteria.size(); i++) {
CustomFilterCriterion item = criteria.get(i);
SubMenu subMenu = menu.addSubMenu(item.name);
if(item.icon != null)
subMenu.setIcon(new BitmapDrawable(item.icon));
for(int j = 0; j < item.entryTitles.length; j++) {
subMenu.add(CustomFilterActivity.MENU_GROUP_FILTER_OPTION,
i, j, item.entryTitles[j]);
}
}
}
// was invoked by short-pressing row
else {
CustomFilterCriterion criterion = menuItemInstance.criterion;
if(criterion.entryTitles == null ||
criterion.entryTitles.length == 0)
return;
menu.setHeaderTitle(criterion.name);
menu.setGroupCheckable(CustomFilterActivity.MENU_GROUP_FILTER_OPTION, true, true);
for(int i = 0; i < criterion.entryTitles.length; i++) {
menu.add(CustomFilterActivity.MENU_GROUP_FILTER_OPTION,
-1, i, criterion.entryTitles[i]);
}
}
}
});
}
// --- listeners and action events
CriterionInstance menuItemInstance = null;
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
for(int i = 0; i < criteria.size(); i++) {
CustomFilterCriterion item = criteria.get(i);
MenuItem menuItem = menu.add(MENU_GROUP_FILTER, i, i, item.name);
if(item.icon != null)
menuItem.setIcon(new BitmapDrawable(item.icon));
if(menu.size() > 0)
menu.clear();
// view holder
if(v.getTag() != null) {
adapter.onCreateContextMenu(menu, v);
}
}
@SuppressWarnings("nls")
void saveAndView() {
StringBuilder sql = new StringBuilder(" WHERE ");
for(int i = 0; i < adapter.getCount(); i++) {
CriterionInstance instance = adapter.getItem(i);
if(instance.selectedIndex < 0 && instance.criterion.entryValues != null)
continue;
switch(instance.type) {
case CriterionInstance.TYPE_ADD:
sql.append("OR ");
break;
case CriterionInstance.TYPE_SUBTRACT:
sql.append("AND NOT ");
break;
case CriterionInstance.TYPE_INTERSECT:
sql.append("AND ");
break;
case CriterionInstance.TYPE_UNIVERSE:
}
// special code for all tasks universe
if(instance.criterion.sql == null)
sql.append(TaskCriteria.activeAndVisible()).append(' ');
else {
String subSql = instance.criterion.sql.replaceAll("%s",
instance.criterion.entryValues[instance.selectedIndex]);
subSql = CustomFilterCriterion.replacePlaceholders(subSql);
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
}
String title;
if(filterName.getText().length() > 0)
title = filterName.getText().toString();
else
title = filterName.getHint().toString();
ContentValues values = new ContentValues(); // TODO
Filter filter = new Filter(title, title, null, values);
filter.sqlQuery = sql.toString();
// TODO save
Intent taskListActivity = new Intent(this, TaskListActivity.class);
taskListActivity.putExtra(TaskListActivity.TOKEN_FILTER, filter);
startActivity(taskListActivity);
}
/**
* Recalculate all sizes
*/
@SuppressWarnings("nls")
void updateList() {
int max = 0, last = -1;
StringBuilder sql = new StringBuilder(Query.select(new CountProperty()).from(Task.TABLE).toString()).
append(" WHERE ");
StringBuilder suggestedTitle = new StringBuilder();
for(int i = 0; i < adapter.getCount(); i++) {
CriterionInstance instance = adapter.getItem(i);
if(instance.selectedIndex < 0 && instance.criterion.entryValues != null) {
instance.start = last;
instance.end = last;
continue;
}
String entryTitle = "";
if(instance.criterion.entryTitles != null) {
entryTitle = instance.criterion.entryTitles[instance.selectedIndex];
}
String title = instance.criterion.text.replace("%s", entryTitle);
switch(instance.type) {
case CriterionInstance.TYPE_ADD:
sql.append("OR ");
title = getString(R.string.CFA_type_add) + " " + title;
break;
case CriterionInstance.TYPE_SUBTRACT:
sql.append("AND NOT ");
title = getString(R.string.CFA_type_subtract) + " " + title;
break;
case CriterionInstance.TYPE_INTERSECT:
sql.append("AND ");
@ -206,18 +344,25 @@ public class CustomFilterActivity extends ListActivity {
case CriterionInstance.TYPE_UNIVERSE:
}
suggestedTitle.append(title).append(' ');
// special code for all tasks universe
if(instance.criterion.sql == null)
sql.append(TaskCriteria.activeAndVisible()).append(' ');
else
sql.append(Task.ID).append(" IN (").append(instance.criterion.sql).append(") ");
else {
String subSql = instance.criterion.sql.replaceAll("%s",
instance.criterion.entryValues[instance.selectedIndex]);
subSql = CustomFilterCriterion.replacePlaceholders(subSql);
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
Cursor cursor = database.getDatabase().rawQuery(sql.toString(), null);
try {
cursor.moveToNext();
max = Math.max(max, cursor.getCount());
instance.start = last == -1 ? cursor.getInt(0) : last;
instance.end = cursor.getInt(0);
last = instance.end;
max = Math.max(max, last);
} finally {
cursor.close();
}
@ -229,21 +374,38 @@ public class CustomFilterActivity extends ListActivity {
}
adapter.notifyDataSetInvalidated();
if(adapter.getCount() > 1 && filterName.getText().length() == 0)
filterName.setHint(suggestedTitle);
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
if(item.getGroupId() == MENU_GROUP_FILTER) {
CustomFilterCriterion criterion = criteria.get(item.getItemId());
CriterionInstance instance = new CriterionInstance();
instance.criterion = criterion;
adapter.add(instance);
if(item.getGroupId() == MENU_GROUP_FILTER_OPTION) {
if(menuItemInstance == null) {
CustomFilterCriterion criterion = criteria.get(item.getItemId());
menuItemInstance = new CriterionInstance();
menuItemInstance.criterion = criterion;
}
menuItemInstance.selectedIndex = item.getOrder();
if(adapter.getPosition(menuItemInstance) == -1)
adapter.add(menuItemInstance);
updateList();
return true;
}
else if(item.getGroupId() == MENU_GROUP_FILTER_OPTION)
return adapter.onMenuItemSelected(item);
else if(item.getGroupId() == MENU_GROUP_CONTEXT_TYPE) {
CriterionInstance instance = adapter.getItem(item.getOrder());
instance.type = item.getItemId();
updateList();
}
else if(item.getGroupId() == MENU_GROUP_CONTEXT_DELETE) {
CriterionInstance instance = adapter.getItem(item.getOrder());
adapter.remove(instance);
updateList();
}
return super.onMenuItemSelected(featureId, item);
}

@ -12,14 +12,11 @@ import android.view.LayoutInflater;
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.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.timsu.astrid.R;
import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.core.CustomFilterActivity.CriterionInstance;
import com.todoroo.astrid.model.AddOn;
@ -53,35 +50,41 @@ public class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
return;
// keep the filter options in the name context menu
viewHolder.name.showContextMenu();
((CustomFilterActivity)activity).menuItemInstance = viewHolder.item;
((CustomFilterActivity)activity).getListView().showContextMenu();
}
};
OnCreateContextMenuListener createContextMenuListener = new OnCreateContextMenuListener() {
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
ViewHolder viewHolder = (ViewHolder) ((View)view.getParent()).getTag();
CustomFilterCriterion criteria = viewHolder.item.criterion;
if(criteria.entryTitles == null ||
criteria.entryTitles.length == 0)
return;
public void onCreateContextMenu(ContextMenu menu, View v) {
// view holder
ViewHolder viewHolder = (ViewHolder) v.getTag();
if(viewHolder == null || viewHolder.item.type == CriterionInstance.TYPE_UNIVERSE)
return;
menu.setHeaderTitle(criteria.name);
menu.setGroupCheckable(CustomFilterActivity.MENU_GROUP_FILTER_OPTION, true, true);
int index = getPosition(viewHolder.item);
for(int i = 0; i < criteria.entryTitles.length; i++) {
menu.add(CustomFilterActivity.MENU_GROUP_FILTER_OPTION,
getPosition(viewHolder.item),
i, criteria.entryTitles[i]);
}
}
};
menu.setHeaderTitle(viewHolder.name.getText());
if(viewHolder.icon.getVisibility() == View.VISIBLE)
menu.setHeaderIcon(viewHolder.icon.getDrawable());
MenuItem item = menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, CriterionInstance.TYPE_INTERSECT, index,
activity.getString(R.string.CFA_context_chain,
activity.getString(R.string.CFA_type_intersect)));
item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_INTERSECT);
item = menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, CriterionInstance.TYPE_ADD, index,
activity.getString(R.string.CFA_context_chain,
activity.getString(R.string.CFA_type_add)));
item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_ADD);
item = menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, CriterionInstance.TYPE_SUBTRACT, index,
activity.getString(R.string.CFA_context_chain,
activity.getString(R.string.CFA_type_subtract)));
item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_SUBTRACT);
menu.setGroupCheckable(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, true, true);
public boolean onMenuItemSelected(MenuItem item) {
CriterionInstance instance = getItem(item.getItemId());
instance.selectedIndex = item.getOrder();
((CustomFilterActivity)activity).updateList();
return true;
menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_DELETE, 0, index,
R.string.CFA_context_delete);
}
// --- view construction
@ -103,8 +106,8 @@ public class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
initializeView(convertView);
// listeners
convertView.setOnCreateContextMenuListener(activity);
convertView.setOnClickListener(filterClickListener);
viewHolder.name.setOnCreateContextMenuListener(createContextMenuListener);
return convertView;
}
@ -123,29 +126,25 @@ public class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
CriterionInstance item = viewHolder.item;
String entryTitle = "";
if(item.selectedIndex >= 0 && item.selectedIndex < item.criterion.entryTitles.length) {
if(item.selectedIndex >= 0 && item.criterion.entryTitles != null &&
item.selectedIndex < item.criterion.entryTitles.length) {
entryTitle = item.criterion.entryTitles[item.selectedIndex];
}
String title = item.criterion.text.replace("%s", entryTitle);
boolean notFirst = getPosition(item) > 1;
viewHolder.type.setVisibility(item.type == CriterionInstance.TYPE_UNIVERSE ?
View.GONE : View.VISIBLE);
switch(item.type) {
case CriterionInstance.TYPE_ADD:
viewHolder.type.setImageResource(R.drawable.arrow_join);
if(notFirst)
title = activity.getString(R.string.CFA_type_add) + " " + title;
title = activity.getString(R.string.CFA_type_add) + " " + title;
break;
case CriterionInstance.TYPE_SUBTRACT:
viewHolder.type.setImageResource(R.drawable.arrow_branch);
if(notFirst)
title = activity.getString(R.string.CFA_type_subtract) + " " + title;
title = activity.getString(R.string.CFA_type_subtract) + " " + title;
break;
case CriterionInstance.TYPE_INTERSECT:
viewHolder.type.setImageResource(R.drawable.arrow_down);
if(notFirst)
title = activity.getString(R.string.CFA_type_intersect) + " " + title;
break;
}

@ -11,8 +11,7 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/CFA_filterName_hint"
android:text="@string/CFA_filterName_new"/>
android:hint="@string/CFA_filterName_hint"/>
<!-- List -->
<ListView android:id="@android:id/list"
@ -40,7 +39,7 @@
android:text="@string/CFA_button_add" />
<Button
android:id="@+id/discard_basic"
android:id="@+id/saveAndView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"

@ -32,12 +32,12 @@
android:layout_height="fill_parent"
android:layout_weight="100"
android:paddingLeft="5dip"
android:textSize="22sp"
android:textSize="18sp"
android:gravity="center_vertical"/>
<!-- filter graphic -->
<com.todoroo.astrid.core.FilterView android:id="@+id/filter"
android:layout_width="64dip"
android:layout_width="64px"
android:layout_height="fill_parent"
android:layout_weight="1"
android:paddingLeft="5dip"

@ -41,8 +41,11 @@
<!-- Filter Criteria Type: intersect (at the begging of title of the criteria) -->
<string name="CFA_type_intersect">also</string>
<!-- Filter Criteria Context Menu: chaining (%s chain type as above) -->
<string name="CFA_context_chain">Chaining: %s</string>
<!-- Filter Criteria Context Menu: delete -->
<string name="CFA_context_delete">Delete</string>
<string name="CFA_context_delete">Delete Row</string>
<!-- Filter Button: add new -->
<string name="CFA_button_add">Add Criteria</string>
@ -66,7 +69,7 @@
</string-array>
<!-- Criteria: importance - display text -->
<string name="CFC_importance_text">Importance >= %s</string>
<string name="CFC_importance_text">Importance: %s</string>
<!-- Criteria: importance - name of criteria -->
<string name="CFC_importance_name">Importance...</string>

Loading…
Cancel
Save