diff --git a/README.md b/README.md
index c66fd814e..933dbc3d3 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Getting Started With Development
3. Use **git** to clone your forked repository (see Github's instructions if you need help). Follow the [Github Line Ending Help](http://help.github.com/dealing-with-lineendings/)
-4. Clone the [astridApi](http://github.com/todoroo/astridApi) github project. This is an (Android Library Project)[http://developer.android.com/guide/developing/eclipse-adt.html#libraryProject] and so you will need ADT version 0.9.7+. Put the astridApi folder in the same root level folder as the astrid folder.
+4. Clone the [astridApi](http://github.com/todoroo/astridApi) github project. This is an [Android Library Project](http://developer.android.com/guide/developing/eclipse-adt.html#libraryProject) and so you will need ADT version 0.9.7+. Put the astridApi folder in the same root level folder as the astrid folder.
4. Open up **eclipse** and import the *astrid*, *astridApi*, and *astrid-tests* projects. There should be no compilation errors. If there are, check the Android page of Eclipse Project Properties to verify the astridApi project was found.
diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml
index 8974026a7..eccbc93f3 100644
--- a/astrid/AndroidManifest.xml
+++ b/astrid/AndroidManifest.xml
@@ -196,6 +196,8 @@
+
+
diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagFilterExposer.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagFilterExposer.java
index 1b053dc03..a4168d86e 100644
--- a/astrid/plugin-src/com/todoroo/astrid/tags/TagFilterExposer.java
+++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagFilterExposer.java
@@ -7,17 +7,21 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.TreeSet;
-import android.content.BroadcastReceiver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
+import android.app.Activity;
+import android.content.*;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
+import android.os.Bundle;
+import android.widget.EditText;
+import android.widget.Toast;
import com.timsu.astrid.R;
+import com.todoroo.andlib.service.Autowired;
+import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.QueryTemplate;
+import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterCategory;
@@ -35,6 +39,8 @@ import com.todoroo.astrid.tags.TagService.Tag;
*/
public class TagFilterExposer extends BroadcastReceiver {
+ private static final String TAG = "tag";
+
private TagService tagService;
private Filter filterFromTag(Context context, Tag tag, Criterion criterion) {
@@ -51,18 +57,24 @@ public class TagFilterExposer extends BroadcastReceiver {
if(tag.count == 0)
filter.color = Color.GRAY;
-// filters[0].contextMenuLabels = new String[] {
-// "Rename Tag",
-// "Delete Tag"
-// };
-// filters[0].contextMenuIntents = new Intent[] {
-// new Intent(),
-// new Intent()
-// };
+ filter.contextMenuLabels = new String[] {
+ context.getString(R.string.tag_cm_rename),
+ context.getString(R.string.tag_cm_delete)
+ };
+ filter.contextMenuIntents = new Intent[] {
+ newTagIntent(context, RenameTagActivity.class, tag),
+ newTagIntent(context, DeleteTagActivity.class, tag)
+ };
return filter;
}
+ private Intent newTagIntent(Context context, Class extends Activity> activity, Tag tag) {
+ Intent ret = new Intent(context, activity);
+ ret.putExtra(TAG, tag.tag);
+ return ret;
+ }
+
@Override
public void onReceive(Context context, Intent intent) {
tagService = TagService.getInstance();
@@ -81,15 +93,17 @@ public class TagFilterExposer extends BroadcastReceiver {
@Override
public int compare(Tag a, Tag b) {
if(a.count == b.count)
- return a.tag.toLowerCase().compareTo(b.tag.toLowerCase());
+ return a.tag.compareTo(b.tag);
return b.count - a.count;
}
});
for(Tag tag : tags) {
if(!actives.containsKey(tag.tag))
tag.count = 0;
- else
+ else {
+ // will decrease tag.count is there are tasks with this tag which are not activeAndVisible but also have not been deleted
tag.count = actives.get(tag.tag);
+ }
sortedTagSet.add(tag);
}
@@ -122,4 +136,115 @@ public class TagFilterExposer extends BroadcastReceiver {
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
+ public abstract static class TagActivity extends Activity {
+
+ protected String tag;
+
+ @Autowired
+ public TagService tagService;
+
+ protected TagActivity() {
+ DependencyInjectionService.getInstance().inject(this);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTheme(android.R.style.Theme_Dialog);
+
+ tag = getIntent().getStringExtra(TAG);
+ if(tag == null) {
+ finish();
+ return;
+ }
+
+ DependencyInjectionService.getInstance().inject(this); // why?
+
+ showDialog();
+ }
+
+ protected DialogInterface.OnClickListener getOkListener() {
+ return new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ if (ok()) {
+ setResult(RESULT_OK);
+ } else {
+ toastNoChanges();
+ setResult(RESULT_CANCELED);
+ }
+ } finally {
+ finish();
+ }
+ }
+ };
+ }
+
+ protected DialogInterface.OnClickListener getCancelListener() {
+ return new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ toastNoChanges();
+ } finally {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ };
+ }
+
+ private void toastNoChanges() {
+ Toast.makeText(this, R.string.TEA_no_tags_modified,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ protected abstract void showDialog();
+
+ protected abstract boolean ok();
+ }
+
+ public static class DeleteTagActivity extends TagActivity {
+
+ @Override
+ protected void showDialog() {
+ DialogUtilities.okCancelDialog(this, getString(R.string.DLG_delete_this_tag_question, tag), getOkListener(), getCancelListener());
+ }
+
+ @Override
+ protected boolean ok() {
+ int deleted = tagService.delete(tag);
+ Toast.makeText(this, getString(R.string.TEA_tags_deleted, tag, deleted),
+ Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ }
+
+ public static class RenameTagActivity extends TagActivity {
+
+ private EditText editor;
+
+ @Override
+ protected void showDialog() {
+ editor = new EditText(this); // not sure why this can't be done in the RenameTagActivity constructor.
+ DialogUtilities.viewDialog(this, getString(R.string.DLG_rename_this_tag_header, tag), editor, getOkListener(), getCancelListener());
+ }
+
+ @Override
+ protected boolean ok() { // this interface is not going to work well with the dialog that says "Are you sure?"
+ String text = editor.getText().toString();
+ if (text == null || text.length() == 0) {
+ return false;
+ } else {
+ int renamed = tagService.rename(tag, text);
+ Toast.makeText(this, getString(R.string.TEA_tags_renamed, tag, text, renamed),
+ Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ }
+ }
+
}
diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java
index 8b9150162..3ca8fa22f 100644
--- a/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java
+++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java
@@ -8,11 +8,7 @@ import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
-import com.todoroo.andlib.sql.Criterion;
-import com.todoroo.andlib.sql.Join;
-import com.todoroo.andlib.sql.Order;
-import com.todoroo.andlib.sql.Query;
-import com.todoroo.andlib.sql.QueryTemplate;
+import com.todoroo.andlib.sql.*;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
@@ -20,6 +16,8 @@ import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.MetadataService;
+import com.todoroo.astrid.service.TaskService;
+import com.todoroo.astrid.utility.Flags;
/**
* Provides operations for working with tags
@@ -53,7 +51,10 @@ public final class TagService {
@Autowired
private MetadataDao metadataDao;
- private TagService() {
+ @Autowired
+ private TaskService taskService;
+
+ public TagService() {
DependencyInjectionService.getInstance().inject(this);
}
@@ -92,12 +93,16 @@ public final class TagService {
*/
public QueryTemplate queryTemplate(Criterion criterion) {
return new QueryTemplate().join(Join.inner(Metadata.TABLE,
- Task.ID.eq(Metadata.TASK))).where(Criterion.and(
- MetadataCriteria.withKey(KEY), TAG.eq(tag),
- criterion));
+ Task.ID.eq(Metadata.TASK))).where(tagEq(tag, criterion));
}
+
}
+ private static Criterion tagEq(String tag, Criterion additionalCriterion) {
+ return Criterion.and(
+ MetadataCriteria.withKey(KEY), TAG.eq(tag),
+ additionalCriterion);
+ }
public QueryTemplate untaggedTemplate() {
return new QueryTemplate().where(Criterion.and(
Criterion.not(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE).where(MetadataCriteria.withKey(KEY)))),
@@ -195,4 +200,35 @@ public final class TagService {
return service.synchronizeMetadata(taskId, metadata, Metadata.KEY.eq(KEY)) > 0;
}
+
+ public int delete(String tag) {
+ invalidateTaskCache(tag);
+ return PluginServices.getMetadataService().deleteWhere(tagEq(tag, Criterion.all));
+ }
+
+ public int rename(String oldTag, String newTag) {
+ // First remove newTag from all tasks that have both oldTag and newTag.
+ MetadataService metadataService = PluginServices.getMetadataService();
+ metadataService.deleteWhere(
+ Criterion.and(
+ Metadata.VALUE1.eq(newTag),
+ Metadata.TASK.in(rowsWithTag(oldTag, Metadata.TASK))));
+
+ // Then rename all instances of oldTag to newTag.
+ Metadata metadata = new Metadata();
+ metadata.setValue(TAG, newTag);
+ int ret = metadataService.update(tagEq(oldTag, Criterion.all), metadata);
+ invalidateTaskCache(newTag);
+ return ret;
+ }
+
+ private Query rowsWithTag(String tag, Field... projections) {
+ return Query.select(projections).from(Metadata.TABLE).where(Metadata.VALUE1.eq(tag));
+ }
+
+ private void invalidateTaskCache(String tag) {
+ taskService.clearDetails(Task.ID.in(rowsWithTag(tag, Task.ID)));
+ Flags.set(Flags.REFRESH);
+ }
+
}
diff --git a/astrid/res/values/strings-core.xml b/astrid/res/values/strings-core.xml
index 612da65cf..964667a7e 100644
--- a/astrid/res/values/strings-core.xml
+++ b/astrid/res/values/strings-core.xml
@@ -321,7 +321,7 @@
Notes will be displayed when you tap a task
- Notes will always displayed
+ Notes will always be displayed
New Task Defaults
diff --git a/astrid/res/values/strings-tags.xml b/astrid/res/values/strings-tags.xml
index e33705159..e35e0dc61 100644
--- a/astrid/res/values/strings-tags.xml
+++ b/astrid/res/values/strings-tags.xml
@@ -28,5 +28,26 @@
Tagged \'%s\'
-
+
+
+ Rename Tag
+
+
+ Delete Tag
+
+
+ Delete this tag: %s? (No tasks will be deleted.)
+
+
+ Rename the tag %s to:
+
+
+ No changes made
+
+
+ Tag %s removed from %d tasks
+
+
+ Replaced %s with %s on %d tasks
+
diff --git a/astrid/src/com/todoroo/astrid/adapter/FilterAdapter.java b/astrid/src/com/todoroo/astrid/adapter/FilterAdapter.java
index df3b0749c..61a9282a4 100644
--- a/astrid/src/com/todoroo/astrid/adapter/FilterAdapter.java
+++ b/astrid/src/com/todoroo/astrid/adapter/FilterAdapter.java
@@ -5,6 +5,8 @@ package com.todoroo.astrid.adapter;
import java.util.ArrayList;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
import android.app.Activity;
import android.content.BroadcastReceiver;
@@ -71,11 +73,14 @@ public class FilterAdapter extends BaseExpandableListAdapter {
/** whether to skip Filters that launch intents instead of being real filters */
private final boolean skipIntentFilters;
- /** queue for loading list sizes */
- private final LinkedBlockingQueue filterQueue = new LinkedBlockingQueue();
-
- /** thread for loading list sizes */
- private Thread filterSizeLoadingThread = null;
+ // Previous solution involved a queue of filters and a filterSizeLoadingThread. The filterSizeLoadingThread had
+ // a few problems: how to make sure that the thread is resumed when the controlling activity is resumed, and
+ // how to make sure that the the filterQueue does not accumulate filters without being processed. I am replacing
+ // both the queue and a the thread with a thread pool, which will shut itself off after a second if it has
+ // nothing to do (corePoolSize == 0, which makes it available for garbage collection), and will wake itself up
+ // if new filters are queued (obviously it cannot be garbage collected if it is possible for new filters to
+ // be added).
+ private ThreadPoolExecutor filterExecutor = new ThreadPoolExecutor(0, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue());
public FilterAdapter(Activity activity, ExpandableListView listView,
int rowLayout, boolean skipIntentFilters) {
@@ -95,34 +100,26 @@ public class FilterAdapter extends BaseExpandableListAdapter {
activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
listView.setGroupIndicator(
activity.getResources().getDrawable(R.drawable.expander_group));
-
- startFilterSizeLoadingThread();
}
- private void startFilterSizeLoadingThread() {
- filterSizeLoadingThread = new Thread() {
+ private void offerFilter(final Filter filter) {
+ filterExecutor.submit(new Runnable() {
@Override
public void run() {
- while(true) {
- try {
- Filter filter = filterQueue.take();
- int size = taskService.countTasks(filter);
- filter.listingTitle = filter.listingTitle + (" (" + //$NON-NLS-1$
- size + ")"); //$NON-NLS-1$
- activity.runOnUiThread(new Runnable() {
- public void run() {
- notifyDataSetInvalidated();
- }
- });
- } catch (InterruptedException e) {
- break;
- } catch (Exception e) {
- Log.e("astrid-filter-adapter", "Error loading filter size", e); //$NON-NLS-1$ //$NON-NLS-2$
- }
+ try {
+ int size = taskService.countTasks(filter);
+ filter.listingTitle = filter.listingTitle + (" (" + //$NON-NLS-1$
+ size + ")"); //$NON-NLS-1$
+ activity.runOnUiThread(new Runnable() {
+ public void run() {
+ notifyDataSetInvalidated();
+ }
+ });
+ } catch (Exception e) {
+ Log.e("astrid-filter-adapter", "Error loading filter size", e); //$NON-NLS-1$ //$NON-NLS-2$
}
}
- };
- filterSizeLoadingThread.start();
+ });
}
public boolean hasStableIds() {
@@ -134,10 +131,10 @@ public class FilterAdapter extends BaseExpandableListAdapter {
// load sizes
if(item instanceof Filter) {
- filterQueue.offer((Filter) item);
+ offerFilter((Filter)item);
} else if(item instanceof FilterCategory) {
for(Filter filter : ((FilterCategory)item).children)
- filterQueue.offer(filter);
+ offerFilter(filter);
}
}
@@ -367,8 +364,6 @@ public class FilterAdapter extends BaseExpandableListAdapter {
*/
public void unregisterRecevier() {
activity.unregisterReceiver(filterReceiver);
- if(filterSizeLoadingThread != null)
- filterSizeLoadingThread.interrupt();
}
/**
diff --git a/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java b/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java
index f95c02d42..803911fc6 100644
--- a/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java
+++ b/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java
@@ -14,6 +14,7 @@ import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.gtasks.GtasksListService;
import com.todoroo.astrid.gtasks.GtasksMetadataService;
import com.todoroo.astrid.gtasks.GtasksPreferenceService;
+import com.todoroo.astrid.tags.TagService;
import com.todoroo.astrid.utility.Constants;
/**
@@ -69,6 +70,9 @@ public class AstridDependencyInjector extends AbstractDependencyInjector {
injectables.put("gtasksListService", GtasksListService.class);
injectables.put("gtasksMetadataService", GtasksMetadataService.class);
+ // com.todoroo.astrid.tags
+ injectables.put("tagService", TagService.class);
+
// these make reference to fields defined above
injectables.put("errorReporters", new ErrorReporter[] {
new AndroidLogReporter(),
diff --git a/astrid/src/com/todoroo/astrid/service/MetadataService.java b/astrid/src/com/todoroo/astrid/service/MetadataService.java
index 8d6baaa5a..c0c8a51fc 100644
--- a/astrid/src/com/todoroo/astrid/service/MetadataService.java
+++ b/astrid/src/com/todoroo/astrid/service/MetadataService.java
@@ -62,8 +62,17 @@ public class MetadataService {
* Delete from metadata table where rows match a certain condition
* @param where
*/
- public void deleteWhere(Criterion where) {
- metadataDao.deleteWhere(where);
+ public int deleteWhere(Criterion where) {
+ return metadataDao.deleteWhere(where);
+ }
+
+ /**
+ * Delete from metadata table where rows match a certain condition
+ * @param where predicate for which rows to update
+ * @param metadata values to set
+ */
+ public int update(Criterion where, Metadata metadata) {
+ return metadataDao.update(where, metadata);
}
/**