diff --git a/astrid/api-src/com/todoroo/astrid/api/CustomFilterCriterion.java b/astrid/api-src/com/todoroo/astrid/api/CustomFilterCriterion.java index 63a274ccd..2ffd476ea 100644 --- a/astrid/api-src/com/todoroo/astrid/api/CustomFilterCriterion.java +++ b/astrid/api-src/com/todoroo/astrid/api/CustomFilterCriterion.java @@ -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 * */ -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 { } }; + } diff --git a/astrid/common-src/com/todoroo/andlib/data/AbstractDatabase.java b/astrid/common-src/com/todoroo/andlib/data/AbstractDatabase.java index b92fd84b0..468c1ff46 100644 --- a/astrid/common-src/com/todoroo/andlib/data/AbstractDatabase.java +++ b/astrid/common-src/com/todoroo/andlib/data/AbstractDatabase.java @@ -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()); + } } /** diff --git a/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java b/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java index 9ecc313a1..beac03a4a 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java @@ -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 criteria = new ArrayList(); @@ -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 startingCriteria = new ArrayList(); 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); } diff --git a/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterAdapter.java b/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterAdapter.java index 01fdfa529..9cdf384bd 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterAdapter.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterAdapter.java @@ -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 { 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 { 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 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; } diff --git a/astrid/res/layout/custom_filter_activity.xml b/astrid/res/layout/custom_filter_activity.xml index 331111af6..ff167ff7d 100644 --- a/astrid/res/layout/custom_filter_activity.xml +++ b/astrid/res/layout/custom_filter_activity.xml @@ -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"/>