Move filter criteria to FilterSettingsActivity

pull/996/head
Alex Baker 5 years ago
parent 2d11a5c55c
commit 3e0a704725

@ -284,15 +284,6 @@
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/TranslucentDialog"/> android:theme="@style/TranslucentDialog"/>
<!-- tags -->
<!-- custom filters -->
<activity
android:name="com.todoroo.astrid.core.CustomFilterActivity"
android:theme="@style/Tasks"/>
<!-- actfm -->
<activity android:name=".activities.TagSettingsActivity"/> <activity android:name=".activities.TagSettingsActivity"/>
<activity android:name=".activities.FilterSettingsActivity"/> <activity android:name=".activities.FilterSettingsActivity"/>

@ -60,6 +60,14 @@ public class CustomFilter extends Filter {
return id; return id;
} }
public String getCriterion() {
return criterion;
}
public void setCriterion(String criterion) {
this.criterion = criterion;
}
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {

@ -1,15 +1,63 @@
package com.todoroo.astrid.core; package com.todoroo.astrid.core;
import static com.google.common.collect.Lists.transform;
import static java.util.Arrays.asList;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.api.CustomFilterCriterion; import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.MultipleSelectCriterion; import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.api.TextInputCriterion; import com.todoroo.astrid.api.TextInputCriterion;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.tasks.filters.FilterCriteriaProvider;
import timber.log.Timber;
public class CriterionInstance { public class CriterionInstance {
static final int TYPE_ADD = 0; public static final int TYPE_ADD = 0;
static final int TYPE_SUBTRACT = 1; public static final int TYPE_SUBTRACT = 1;
static final int TYPE_INTERSECT = 2; public static final int TYPE_INTERSECT = 2;
static final int TYPE_UNIVERSE = 3; public static final int TYPE_UNIVERSE = 3;
public static List<CriterionInstance> fromString(
FilterCriteriaProvider provider, String criterion) {
if (Strings.isNullOrEmpty(criterion)) {
return Collections.emptyList();
}
List<CriterionInstance> entries = new ArrayList<>();
for (String row : criterion.split("\n")) {
CriterionInstance entry = new CriterionInstance();
List<String> split =
transform(
Splitter.on(AndroidUtilities.SERIALIZATION_SEPARATOR).splitToList(row),
CriterionInstance::unescape);
if (split.size() != 4 && split.size() != 5) {
Timber.e("invalid row: %s", row);
return Collections.emptyList();
}
entry.criterion = provider.getFilterCriteria(split.get(0));
String value = split.get(1);
if (entry.criterion instanceof TextInputCriterion) {
entry.selectedText = value;
} else if (entry.criterion instanceof MultipleSelectCriterion) {
MultipleSelectCriterion multipleSelectCriterion = (MultipleSelectCriterion) entry.criterion;
if (multipleSelectCriterion.entryValues != null) {
entry.selectedIndex = asList(multipleSelectCriterion.entryValues).indexOf(value);
}
} else {
Timber.d("Ignored value %s for %s", value, entry.criterion);
}
entry.type = Integer.parseInt(split.get(3));
entry.criterion.sql = split.get(4);
Timber.d("%s -> %s", row, entry);
entries.add(entry);
}
return entries;
}
/** criteria for this instance */ /** criteria for this instance */
public CustomFilterCriterion criterion; public CustomFilterCriterion criterion;
@ -25,9 +73,9 @@ public class CriterionInstance {
public int end; public int end;
/** statistics for filter count */ /** statistics for filter count */
int start; public int start;
int max; public int max;
String getTitleFromCriterion() { String getTitleFromCriterion() {
if (criterion instanceof MultipleSelectCriterion) { if (criterion instanceof MultipleSelectCriterion) {
@ -47,7 +95,7 @@ public class CriterionInstance {
throw new UnsupportedOperationException("Unknown criterion type"); // $NON-NLS-1$ throw new UnsupportedOperationException("Unknown criterion type"); // $NON-NLS-1$
} }
String getValueFromCriterion() { public String getValueFromCriterion() {
if (type == TYPE_UNIVERSE) { if (type == TYPE_UNIVERSE) {
return null; return null;
} }
@ -84,4 +132,20 @@ public class CriterionInstance {
+ max + max
+ '}'; + '}';
} }
public static String escape(String item) {
if (item == null) {
return ""; // $NON-NLS-1$
}
return item.replace(
AndroidUtilities.SERIALIZATION_SEPARATOR, AndroidUtilities.SEPARATOR_ESCAPE);
}
private static String unescape(String item) {
if (Strings.isNullOrEmpty(item)) {
return "";
}
return item.replace(
AndroidUtilities.SEPARATOR_ESCAPE, AndroidUtilities.SERIALIZATION_SEPARATOR);
}
} }

@ -1,434 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.core;
import static android.text.TextUtils.isEmpty;
import static com.google.common.collect.Lists.transform;
import static com.todoroo.andlib.utility.AndroidUtilities.mapToSerializedString;
import static java.util.Arrays.asList;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.todoroo.andlib.data.Property.CountProperty;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.sql.UnaryCriterion;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.activity.MainActivity;
import com.todoroo.astrid.api.CustomFilter;
import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.api.TextInputCriterion;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.data.FilterDao;
import org.tasks.databinding.CustomFilterActivityBinding;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.filters.FilterCriteriaProvider;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.ThemedInjectingAppCompatActivity;
import org.tasks.locale.Locale;
import timber.log.Timber;
/**
* Activity that allows users to build custom filters
*
* @author Tim Su <tim@todoroo.com>
*/
public class CustomFilterActivity extends ThemedInjectingAppCompatActivity
implements Toolbar.OnMenuItemClickListener {
static final int MENU_GROUP_CONTEXT_TYPE = 1;
static final int MENU_GROUP_CONTEXT_DELETE = 2;
private static final String EXTRA_CRITERIA = "extra_criteria";
private static final int MENU_GROUP_FILTER = 0;
@Inject Database database;
@Inject FilterDao filterDao;
@Inject DialogBuilder dialogBuilder;
@Inject FilterCriteriaProvider filterCriteriaProvider;
@Inject Locale locale;
private CustomFilterActivityBinding binding;
private ListView listView;
private CustomFilterAdapter adapter;
private static String serializeFilters(CustomFilterAdapter adapter) {
List<String> rows = new ArrayList<>();
for (int i = 0; i < adapter.getCount(); i++) {
CriterionInstance item = adapter.getItem(i);
// criterion|entry|text|type|sql
String row =
Joiner.on(AndroidUtilities.SERIALIZATION_SEPARATOR)
.join(
asList(
escape(item.criterion.identifier),
escape(item.getValueFromCriterion()),
escape(item.criterion.text),
item.type,
item.criterion.sql == null ? "" : item.criterion.sql));
Timber.d("%s -> %s", item, row);
rows.add(row);
}
return Joiner.on("\n").join(rows);
}
private List<CriterionInstance> deserializeCriterion(@Nullable String criterion) {
if (Strings.isNullOrEmpty(criterion)) {
return Collections.emptyList();
}
List<CriterionInstance> entries = new ArrayList<>();
for (String row : criterion.split("\n")) {
CriterionInstance entry = new CriterionInstance();
List<String> split =
transform(
Splitter.on(AndroidUtilities.SERIALIZATION_SEPARATOR).splitToList(row),
CustomFilterActivity::unescape);
if (split.size() != 4 && split.size() != 5) {
Timber.e("invalid row: %s", row);
return Collections.emptyList();
}
entry.criterion = filterCriteriaProvider.getFilterCriteria(split.get(0));
String value = split.get(1);
if (entry.criterion instanceof TextInputCriterion) {
entry.selectedText = value;
} else if (entry.criterion instanceof MultipleSelectCriterion) {
MultipleSelectCriterion multipleSelectCriterion = (MultipleSelectCriterion) entry.criterion;
if (multipleSelectCriterion.entryValues != null) {
entry.selectedIndex = asList(multipleSelectCriterion.entryValues).indexOf(value);
}
} else {
Timber.d("Ignored value %s for %s", value, entry.criterion);
}
entry.type = Integer.parseInt(split.get(3));
entry.criterion.sql = split.get(4);
Timber.d("%s -> %s", row, entry);
entries.add(entry);
}
return entries;
}
private static String escape(String item) {
if (item == null) {
return ""; // $NON-NLS-1$
}
return item.replace(
AndroidUtilities.SERIALIZATION_SEPARATOR, AndroidUtilities.SEPARATOR_ESCAPE);
}
private static String unescape(String item) {
if (Strings.isNullOrEmpty(item)) {
return "";
}
return item.replace(
AndroidUtilities.SEPARATOR_ESCAPE, AndroidUtilities.SERIALIZATION_SEPARATOR);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_CRITERIA, serializeFilters(adapter));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = CustomFilterActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Toolbar toolbar = binding.toolbar.toolbar;
toolbar.setNavigationIcon(ContextCompat.getDrawable(this, R.drawable.ic_outline_clear_24px));
toolbar.setTitle(R.string.FLA_new_filter);
toolbar.inflateMenu(R.menu.menu_custom_filter_activity);
toolbar.setOnMenuItemClickListener(this);
toolbar.setNavigationOnClickListener(view -> discard());
themeColor.apply(toolbar);
listView = findViewById(android.R.id.list);
List<CriterionInstance> criteria =
new ArrayList<>(
deserializeCriterion(
savedInstanceState == null
? getIntent().getStringExtra(EXTRA_CRITERIA)
: savedInstanceState.getString(EXTRA_CRITERIA)));
if (criteria.isEmpty()) {
CriterionInstance instance = new CriterionInstance();
instance.criterion = filterCriteriaProvider.getStartingUniverse();
instance.type = CriterionInstance.TYPE_UNIVERSE;
criteria.add(instance);
}
adapter = new CustomFilterAdapter(this, dialogBuilder, criteria, locale);
listView.setAdapter(adapter);
updateList();
setUpListeners();
}
@Override
public void inject(ActivityComponent component) {
component.inject(this);
}
private void setUpListeners() {
findViewById(R.id.add).setOnClickListener(v -> listView.showContextMenu());
listView.setOnCreateContextMenuListener(
(menu, v, menuInfo) -> {
if (menu.hasVisibleItems()) {
/* If it has items already, then the user did not click on the "Add Criteria" button, but instead
long held on a row in the list view, which caused CustomFilterAdapter.onCreateContextMenu
to be invoked before this onCreateContextMenu method was invoked.
*/
return;
}
int i = 0;
for (CustomFilterCriterion item : filterCriteriaProvider.getAll()) {
menu.add(CustomFilterActivity.MENU_GROUP_FILTER, i, 0, item.name);
i++;
}
});
}
// --- listeners and action events
@Override
public void finish() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(binding.filterName.getWindowToken(), 0);
super.finish();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (menu.size() > 0) {
menu.clear();
}
// view holder
if (v.getTag() != null) {
adapter.onCreateContextMenu(menu, v);
}
}
private void saveAndView() {
String title = binding.filterName.getText().toString().trim();
if (isEmpty(title)) {
return;
}
StringBuilder sql = new StringBuilder(" WHERE ");
Map<String, Object> values = new HashMap<>();
for (int i = 0; i < adapter.getCount(); i++) {
CriterionInstance instance = adapter.getItem(i);
String value = instance.getValueFromCriterion();
if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) {
value = "";
}
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.replace("?", UnaryCriterion.sanitize(value));
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
if (instance.criterion.valuesForNewTasks != null
&& instance.type == CriterionInstance.TYPE_INTERSECT) {
for (Entry<String, Object> entry : instance.criterion.valuesForNewTasks.entrySet()) {
values.put(
entry.getKey().replace("?", value), entry.getValue().toString().replace("?", value));
}
}
}
org.tasks.data.Filter filter = persist(title, sql.toString(), values);
Filter customFilter = new CustomFilter(filter);
setResult(RESULT_OK, new Intent().putExtra(MainActivity.OPEN_FILTER, customFilter));
finish();
}
/** Recalculate all sizes */
void updateList() {
int max = 0, last = -1;
StringBuilder sql =
new StringBuilder(Query.select(new CountProperty()).from(Task.TABLE).toString())
.append(" WHERE ");
for (int i = 0; i < adapter.getCount(); i++) {
CriterionInstance instance = adapter.getItem(i);
String value = instance.getValueFromCriterion();
if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) {
value = "";
}
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;
}
// special code for all tasks universe
if (instance.type == CriterionInstance.TYPE_UNIVERSE || instance.criterion.sql == null) {
sql.append(TaskCriteria.activeAndVisible()).append(' ');
} else {
String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value));
subSql = PermaSql.replacePlaceholdersForQuery(subSql);
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
Cursor cursor = database.query(sql.toString(), null);
try {
cursor.moveToNext();
instance.start = last == -1 ? cursor.getInt(0) : last;
instance.end = cursor.getInt(0);
last = instance.end;
max = Math.max(max, last);
} finally {
cursor.close();
}
}
for (int i = 0; i < adapter.getCount(); i++) {
CriterionInstance instance = adapter.getItem(i);
instance.max = max;
}
adapter.notifyDataSetInvalidated();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.menu_save) {
saveAndView();
return true;
}
return onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
discard();
}
private void discard() {
if (binding.filterName.getText().toString().trim().isEmpty() && adapter.getCount() <= 1) {
finish();
} else {
dialogBuilder
.newDialog(R.string.discard_changes)
.setPositiveButton(R.string.keep_editing, null)
.setNegativeButton(R.string.discard, (dialog, which) -> finish())
.show();
}
}
@Override
public boolean onContextItemSelected(android.view.MenuItem item) {
if (item.getGroupId() == MENU_GROUP_FILTER) {
// give an initial value for the row before adding it
CustomFilterCriterion criterion = filterCriteriaProvider.getAll().get(item.getItemId());
final CriterionInstance instance = new CriterionInstance();
instance.criterion = criterion;
adapter.showOptionsFor(
instance,
() -> {
adapter.add(instance);
updateList();
});
return true;
}
// item type context item
else if (item.getGroupId() == MENU_GROUP_CONTEXT_TYPE) {
CriterionInstance instance = adapter.getItem(item.getOrder());
instance.type = item.getItemId();
updateList();
}
// delete context item
else if (item.getGroupId() == MENU_GROUP_CONTEXT_DELETE) {
CriterionInstance instance = adapter.getItem(item.getOrder());
adapter.remove(instance);
updateList();
}
return super.onContextItemSelected(item);
}
private org.tasks.data.Filter persist(String title, String sql, Map<String, Object> values) {
if (title == null || title.length() == 0) {
return null;
}
// if filter of this name exists, edit it
org.tasks.data.Filter storeObject = filterDao.getByName(title);
if (storeObject == null) {
storeObject = new org.tasks.data.Filter();
}
// populate saved filter properties
storeObject.setTitle(title);
storeObject.setSql(sql);
storeObject.setValues(values == null ? "" : mapToSerializedString(values));
storeObject.setCriterion(serializeFilters(adapter));
storeObject.setId(filterDao.insertOrUpdate(storeObject));
return storeObject.getId() >= 0 ? storeObject : null;
}
}

@ -6,6 +6,9 @@
package com.todoroo.astrid.core; package com.todoroo.astrid.core;
import static com.todoroo.astrid.core.CriterionInstance.escape;
import static java.util.Arrays.asList;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -18,22 +21,33 @@ import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.base.Joiner;
import com.todoroo.andlib.sql.UnaryCriterion;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.api.MultipleSelectCriterion; import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.api.TextInputCriterion; import com.todoroo.astrid.api.TextInputCriterion;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.tasks.R; import org.tasks.R;
import org.tasks.activities.FilterSettingsActivity;
import org.tasks.dialogs.AlertDialogBuilder; import org.tasks.dialogs.AlertDialogBuilder;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.locale.Locale; import org.tasks.locale.Locale;
import timber.log.Timber;
/** /**
* Adapter for AddOns * Adapter for AddOns
* *
* @author Tim Su <tim@todoroo.com> * @author Tim Su <tim@todoroo.com>
*/ */
class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> { public class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
private final CustomFilterActivity activity; private final FilterSettingsActivity activity;
private final DialogBuilder dialogBuilder; private final DialogBuilder dialogBuilder;
private final LayoutInflater inflater; private final LayoutInflater inflater;
private final Locale locale; private final Locale locale;
@ -60,8 +74,8 @@ class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
// --- view event handling // --- view event handling
CustomFilterAdapter( public CustomFilterAdapter(
CustomFilterActivity activity, FilterSettingsActivity activity,
DialogBuilder dialogBuilder, DialogBuilder dialogBuilder,
List<CriterionInstance> objects, List<CriterionInstance> objects,
Locale locale) { Locale locale) {
@ -72,7 +86,7 @@ class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
inflater = activity.getLayoutInflater(); inflater = activity.getLayoutInflater();
} }
void onCreateContextMenu(ContextMenu menu, View v) { public void onCreateContextMenu(ContextMenu menu, View v) {
// view holder // view holder
ViewHolder viewHolder = (ViewHolder) v.getTag(); ViewHolder viewHolder = (ViewHolder) v.getTag();
if (viewHolder == null || viewHolder.item.type == CriterionInstance.TYPE_UNIVERSE) { if (viewHolder == null || viewHolder.item.type == CriterionInstance.TYPE_UNIVERSE) {
@ -85,7 +99,7 @@ class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
MenuItem item = MenuItem item =
menu.add( menu.add(
CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, FilterSettingsActivity.MENU_GROUP_CONTEXT_TYPE,
CriterionInstance.TYPE_INTERSECT, CriterionInstance.TYPE_INTERSECT,
index, index,
activity.getString( activity.getString(
@ -93,7 +107,7 @@ class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_INTERSECT); item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_INTERSECT);
item = item =
menu.add( menu.add(
CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, FilterSettingsActivity.MENU_GROUP_CONTEXT_TYPE,
CriterionInstance.TYPE_ADD, CriterionInstance.TYPE_ADD,
index, index,
activity.getString( activity.getString(
@ -102,19 +116,19 @@ class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
item = item =
menu.add( menu.add(
CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, FilterSettingsActivity.MENU_GROUP_CONTEXT_TYPE,
CriterionInstance.TYPE_SUBTRACT, CriterionInstance.TYPE_SUBTRACT,
index, index,
activity.getString( activity.getString(
R.string.CFA_context_chain, activity.getString(R.string.CFA_type_subtract))); R.string.CFA_context_chain, activity.getString(R.string.CFA_type_subtract)));
item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_SUBTRACT); item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_SUBTRACT);
menu.setGroupCheckable(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, true, true); menu.setGroupCheckable(FilterSettingsActivity.MENU_GROUP_CONTEXT_TYPE, true, true);
menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_DELETE, 0, index, R.string.CFA_context_delete); menu.add(FilterSettingsActivity.MENU_GROUP_CONTEXT_DELETE, 0, index, R.string.CFA_context_delete);
} }
/** Show options menu for the given criterioninstance */ /** Show options menu for the given criterioninstance */
void showOptionsFor(final CriterionInstance item, final Runnable onComplete) { public void showOptionsFor(final CriterionInstance item, final Runnable onComplete) {
AlertDialogBuilder dialog = dialogBuilder.newDialog(item.criterion.name); AlertDialogBuilder dialog = dialogBuilder.newDialog(item.criterion.name);
if (item.criterion instanceof MultipleSelectCriterion) { if (item.criterion instanceof MultipleSelectCriterion) {
@ -205,6 +219,80 @@ class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
viewHolder.filterCount.setText(locale.formatNumber(item.end)); viewHolder.filterCount.setText(locale.formatNumber(item.end));
} }
private String getValue(CriterionInstance instance) {
String value = instance.getValueFromCriterion();
if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) {
value = "";
}
return value;
}
public String getSql() {
StringBuilder sql = new StringBuilder(" WHERE ");
for (int i = 0; i < getCount(); i++) {
CriterionInstance instance = getItem(i);
String value = getValue(instance);
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;
}
// special code for all tasks universe
if (instance.type == CriterionInstance.TYPE_UNIVERSE || instance.criterion.sql == null) {
sql.append(TaskCriteria.activeAndVisible()).append(' ');
} else {
String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value));
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
}
return sql.toString();
}
public Map<String, Object> getValues() {
Map<String, Object> values = new HashMap<>();
for (int i = 0; i < getCount(); i++) {
CriterionInstance instance = getItem(i);
String value = getValue(instance);
if (instance.criterion.valuesForNewTasks != null
&& instance.type == CriterionInstance.TYPE_INTERSECT) {
for (Entry<String, Object> entry : instance.criterion.valuesForNewTasks.entrySet()) {
values.put(
entry.getKey().replace("?", value), entry.getValue().toString().replace("?", value));
}
}
}
return values;
}
public String getCriterion() {
List<String> rows = new ArrayList<>();
for (int i = 0; i < getCount(); i++) {
CriterionInstance item = getItem(i);
// criterion|entry|text|type|sql
String row =
Joiner.on(AndroidUtilities.SERIALIZATION_SEPARATOR)
.join(
asList(
escape(item.criterion.identifier),
escape(item.getValueFromCriterion()),
escape(item.criterion.text),
item.type,
item.criterion.sql == null ? "" : item.criterion.sql));
Timber.d("%s -> %s", item, row);
rows.add(row);
}
return Joiner.on("\n").join(rows);
}
private static class ViewHolder { private static class ViewHolder {
CriterionInstance item; CriterionInstance item;
ImageView type; ImageView type;

@ -10,25 +10,55 @@ import static android.text.TextUtils.isEmpty;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.ListView;
import butterknife.BindView; import butterknife.BindView;
import butterknife.OnClick;
import butterknife.OnTextChanged; import butterknife.OnTextChanged;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import com.todoroo.andlib.data.Property.CountProperty;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.sql.UnaryCriterion;
import com.todoroo.astrid.activity.MainActivity; import com.todoroo.astrid.activity.MainActivity;
import com.todoroo.astrid.activity.TaskListFragment; import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.api.CustomFilter; import com.todoroo.astrid.api.CustomFilter;
import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.core.CriterionInstance;
import com.todoroo.astrid.core.CustomFilterAdapter;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
import org.tasks.data.FilterDao; import org.tasks.data.FilterDao;
import org.tasks.filters.FilterCriteriaProvider;
import org.tasks.injection.ActivityComponent; import org.tasks.injection.ActivityComponent;
import org.tasks.locale.Locale;
public class FilterSettingsActivity extends BaseListSettingsActivity { public class FilterSettingsActivity extends BaseListSettingsActivity {
public static final int MENU_GROUP_CONTEXT_TYPE = 1;
public static final int MENU_GROUP_CONTEXT_DELETE = 2;
private static final int MENU_GROUP_FILTER = 0;
public static final String TOKEN_FILTER = "token_filter"; public static final String TOKEN_FILTER = "token_filter";
public static final String EXTRA_CRITERIA = "extra_criteria";
@Inject FilterDao filterDao; @Inject FilterDao filterDao;
@Inject Locale locale;
@Inject Database database;
@Inject FilterCriteriaProvider filterCriteriaProvider;
@BindView(R.id.name) @BindView(R.id.name)
TextInputEditText name; TextInputEditText name;
@ -36,32 +66,96 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
@BindView(R.id.name_layout) @BindView(R.id.name_layout)
TextInputLayout nameLayout; TextInputLayout nameLayout;
@BindView(R.id.list)
ListView listView; // TODO: convert to recycler view
@BindView(R.id.fab)
ExtendedFloatingActionButton fab;
private CustomFilter filter; private CustomFilter filter;
private CustomFilterAdapter adapter;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
filter = getIntent().getParcelableExtra(TOKEN_FILTER); filter = getIntent().getParcelableExtra(TOKEN_FILTER);
if (filter == null) {
org.tasks.data.Filter f = new org.tasks.data.Filter();
f.setSql("");
this.filter = new CustomFilter(f);
}
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (savedInstanceState == null) { if (savedInstanceState == null && filter != null) {
selectedColor = filter.tint; selectedColor = filter.tint;
selectedIcon = filter.icon; selectedIcon = filter.icon;
name.setText(filter.listingTitle);
}
List<CriterionInstance> criteria =
new ArrayList<>(
CriterionInstance.fromString(
filterCriteriaProvider,
savedInstanceState == null
? filter.getCriterion()
: savedInstanceState.getString(EXTRA_CRITERIA)));
if (criteria.isEmpty()) {
CriterionInstance instance = new CriterionInstance();
instance.criterion = filterCriteriaProvider.getStartingUniverse();
instance.type = CriterionInstance.TYPE_UNIVERSE;
criteria.add(instance);
} }
adapter = new CustomFilterAdapter(this, dialogBuilder, criteria, locale);
fab.setExtended(adapter.getCount() <= 1);
listView.setAdapter(adapter);
name.setText(filter.listingTitle); updateList();
setUpListeners();
updateTheme(); updateTheme();
} }
@OnClick(R.id.fab)
void addCriteria() {
listView.showContextMenu();
fab.shrink();
}
private void setUpListeners() {
listView.setOnCreateContextMenuListener(
(menu, v, menuInfo) -> {
if (menu.hasVisibleItems()) {
/* If it has items already, then the user did not click on the "Add Criteria" button, but instead
long held on a row in the list view, which caused CustomFilterAdapter.onCreateContextMenu
to be invoked before this onCreateContextMenu method was invoked.
*/
return;
}
int i = 0;
for (CustomFilterCriterion item : filterCriteriaProvider.getAll()) {
menu.add(MENU_GROUP_FILTER, i, 0, item.name);
i++;
}
});
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_CRITERIA, adapter.getCriterion());
}
@Override @Override
protected boolean isNew() { protected boolean isNew() {
return false; return filter.getId() == 0;
} }
@Override @Override
protected String getToolbarTitle() { protected String getToolbarTitle() {
return filter.listingTitle; return isNew() ? getString(R.string.FLA_new_filter) : filter.listingTitle;
} }
@OnTextChanged(R.id.name) @OnTextChanged(R.id.name)
@ -87,7 +181,13 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
filter.listingTitle = newName; filter.listingTitle = newName;
filter.tint = selectedColor; filter.tint = selectedColor;
filter.icon = selectedIcon; filter.icon = selectedIcon;
filterDao.update(filter.toStoreObject()); filter.sqlQuery = adapter.getSql();
filter.valuesForNewTasks.clear();
for (Map.Entry<String, Object> entry : adapter.getValues().entrySet()) {
filter.valuesForNewTasks.put(entry.getKey(), entry.getValue());
}
filter.setCriterion(adapter.getCriterion());
filterDao.insertOrUpdate(filter.toStoreObject());
setResult( setResult(
RESULT_OK, RESULT_OK,
new Intent(TaskListFragment.ACTION_RELOAD).putExtra(MainActivity.OPEN_FILTER, filter)); new Intent(TaskListFragment.ACTION_RELOAD).putExtra(MainActivity.OPEN_FILTER, filter));
@ -104,7 +204,10 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
protected boolean hasChanges() { protected boolean hasChanges() {
return !(getNewName().equals(filter.listingTitle) return !(getNewName().equals(filter.listingTitle)
&& selectedColor == filter.tint && selectedColor == filter.tint
&& selectedIcon == filter.icon); && selectedIcon == filter.icon
&& adapter.getSql().equals(filter.sqlQuery)
&& adapter.getValues().equals(filter.valuesForNewTasks)
&& adapter.getCriterion().equals(filter.getCriterion()));
} }
@Override @Override
@ -127,4 +230,104 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
new Intent(TaskListFragment.ACTION_DELETED).putExtra(TOKEN_FILTER, filter)); new Intent(TaskListFragment.ACTION_DELETED).putExtra(TOKEN_FILTER, filter));
finish(); finish();
} }
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (menu.size() > 0) {
menu.clear();
}
// view holder
if (v.getTag() != null) {
adapter.onCreateContextMenu(menu, v);
}
}
@Override
public boolean onContextItemSelected(android.view.MenuItem item) {
if (item.getGroupId() == MENU_GROUP_FILTER) {
// give an initial value for the row before adding it
CustomFilterCriterion criterion = filterCriteriaProvider.getAll().get(item.getItemId());
final CriterionInstance instance = new CriterionInstance();
instance.criterion = criterion;
adapter.showOptionsFor(
instance,
() -> {
adapter.add(instance);
updateList();
});
return true;
}
// item type context item
else if (item.getGroupId() == MENU_GROUP_CONTEXT_TYPE) {
CriterionInstance instance = adapter.getItem(item.getOrder());
instance.type = item.getItemId();
updateList();
}
// delete context item
else if (item.getGroupId() == MENU_GROUP_CONTEXT_DELETE) {
CriterionInstance instance = adapter.getItem(item.getOrder());
adapter.remove(instance);
updateList();
}
return super.onContextItemSelected(item);
}
public void updateList() {
int max = 0, last = -1;
StringBuilder sql =
new StringBuilder(Query.select(new CountProperty()).from(Task.TABLE).toString())
.append(" WHERE ");
for (int i = 0; i < adapter.getCount(); i++) {
CriterionInstance instance = adapter.getItem(i);
String value = instance.getValueFromCriterion();
if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) {
value = "";
}
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;
}
// special code for all tasks universe
if (instance.type == CriterionInstance.TYPE_UNIVERSE || instance.criterion.sql == null) {
sql.append(TaskCriteria.activeAndVisible()).append(' ');
} else {
String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value));
subSql = PermaSql.replacePlaceholdersForQuery(subSql);
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
Cursor cursor = database.query(sql.toString(), null);
try {
cursor.moveToNext();
instance.start = last == -1 ? cursor.getInt(0) : last;
instance.end = cursor.getInt(0);
last = instance.end;
max = Math.max(max, last);
} finally {
cursor.close();
}
}
for (int i = 0; i < adapter.getCount(); i++) {
CriterionInstance instance = adapter.getItem(i);
instance.max = max;
}
adapter.notifyDataSetInvalidated();
}
} }

@ -14,7 +14,6 @@ import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.MultipleSelectCriterion; import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.api.PermaSql; import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.api.TextInputCriterion; import com.todoroo.astrid.api.TextInputCriterion;
import com.todoroo.astrid.core.CustomFilterActivity.CriterionInstance;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.Task.Priority; import com.todoroo.astrid.data.Task.Priority;

@ -20,7 +20,6 @@ import com.google.common.collect.Iterables;
import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.core.BuiltInFilterExposer; import com.todoroo.astrid.core.BuiltInFilterExposer;
import com.todoroo.astrid.core.CustomFilterActivity;
import com.todoroo.astrid.core.CustomFilterExposer; import com.todoroo.astrid.core.CustomFilterExposer;
import com.todoroo.astrid.timers.TimerFilterExposer; import com.todoroo.astrid.timers.TimerFilterExposer;
import java.util.ArrayList; import java.util.ArrayList;
@ -34,6 +33,7 @@ import javax.inject.Inject;
import org.tasks.BuildConfig; import org.tasks.BuildConfig;
import org.tasks.Function; import org.tasks.Function;
import org.tasks.R; import org.tasks.R;
import org.tasks.activities.FilterSettingsActivity;
import org.tasks.activities.GoogleTaskListSettingsActivity; import org.tasks.activities.GoogleTaskListSettingsActivity;
import org.tasks.activities.TagSettingsActivity; import org.tasks.activities.TagSettingsActivity;
import org.tasks.billing.Inventory; import org.tasks.billing.Inventory;
@ -140,7 +140,7 @@ public class FilterProvider {
new NavigationDrawerAction( new NavigationDrawerAction(
context.getString(R.string.FLA_new_filter), context.getString(R.string.FLA_new_filter),
R.drawable.ic_outline_add_24px, R.drawable.ic_outline_add_24px,
new Intent(context, CustomFilterActivity.class), new Intent(context, FilterSettingsActivity.class),
NavigationDrawerFragment.REQUEST_NEW_LIST)); NavigationDrawerFragment.REQUEST_NEW_LIST));
} }
} }

@ -4,7 +4,6 @@ import com.todoroo.astrid.activity.BeastModePreferences;
import com.todoroo.astrid.activity.MainActivity; import com.todoroo.astrid.activity.MainActivity;
import com.todoroo.astrid.activity.ShareLinkActivity; import com.todoroo.astrid.activity.ShareLinkActivity;
import com.todoroo.astrid.activity.TaskEditActivity; import com.todoroo.astrid.activity.TaskEditActivity;
import com.todoroo.astrid.core.CustomFilterActivity;
import com.todoroo.astrid.gcal.CalendarReminderActivity; import com.todoroo.astrid.gcal.CalendarReminderActivity;
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity; import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity;
import dagger.Subcomponent; import dagger.Subcomponent;
@ -56,8 +55,6 @@ public interface ActivityComponent {
void inject(DashClockSettings dashClockSettings); void inject(DashClockSettings dashClockSettings);
void inject(CustomFilterActivity customFilterActivity);
void inject(CalendarReminderActivity calendarReminderActivity); void inject(CalendarReminderActivity calendarReminderActivity);
void inject(FilterSettingsActivity filterSettingsActivity); void inject(FilterSettingsActivity filterSettingsActivity);

@ -1,86 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
** Copyright (c) 2012 Todoroo Inc
**
** See the file "LICENSE" for the full license governing this code.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<include
android:id="@+id/toolbar"
layout="@layout/toolbar"/>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/tag_label"
style="@style/TextAppearance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/display_name"/>
<EditText
android:id="@+id/filter_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_below="@id/tag_label"
android:background="#00000000"
android:gravity="start"
android:hint="@string/enter_filter_name"
android:imeOptions="flagNoExtractUi"
android:inputType="textCapSentences"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:textColorHint"
android:textSize="15sp"/>
</RelativeLayout>
<!-- List -->
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:cacheColorHint="#00000000"
android:scrollbars="vertical"/>
<!-- help text -->
<TextView
style="@style/TextAppearance"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:gravity="center"
android:text="@string/CFA_help"/>
<!-- buttons -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:padding="5dip"
android:baselineAligned="false"
android:orientation="horizontal">
<Button
android:id="@+id/add"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="3dp"
android:paddingTop="10dip"
android:paddingBottom="10dip"
android:text="@string/CFA_button_add"/>
</LinearLayout>
</LinearLayout>

@ -1,19 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> xmlns:app="http://schemas.android.com/apk/res-auto">
<include layout="@layout/toolbar"/> <include layout="@layout/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"/>
<ScrollView <androidx.core.widget.NestedScrollView
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="top"> android:layout_below="@id/toolbar"
android:fillViewport="true">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:descendantFocusability="beforeDescendants" android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:orientation="vertical"> android:orientation="vertical">
@ -37,8 +41,37 @@
<include layout="@layout/list_settings_icon"/> <include layout="@layout/list_settings_icon"/>
<!-- help text -->
<TextView
style="@style/TextAppearance"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:gravity="center"
android:text="@string/CFA_help"/>
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cacheColorHint="#00000000"
android:scrollbars="vertical"
android:nestedScrollingEnabled="true"/>
</LinearLayout> </LinearLayout>
</ScrollView> </androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/keyline_first"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:text="@string/CFA_button_add"
app:icon="@drawable/ic_outline_add_24px"
app:borderWidth="0dp"/>
</LinearLayout> </RelativeLayout>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_save"
android:icon="@drawable/ic_outline_save_24px"
android:title="@string/save"
app:showAsAction="always"/>
</menu>
Loading…
Cancel
Save