Prevent duplicate tag creation

Closes #163
pull/384/head
Alex Baker 9 years ago
parent a781bdb6e4
commit 6f9d2ae273

@ -13,10 +13,14 @@ import android.os.Bundle;
import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView;
import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.AndroidUtilities;
@ -58,6 +62,7 @@ public class TagSettingsActivity extends InjectingAppCompatActivity {
@Bind(R.id.tag_name) EditText tagName; @Bind(R.id.tag_name) EditText tagName;
@Bind(R.id.toolbar) Toolbar toolbar; @Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.tag_error) TextView tagError;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -90,39 +95,55 @@ public class TagSettingsActivity extends InjectingAppCompatActivity {
tagName.setText(autopopulateName); tagName.setText(autopopulateName);
getIntent().removeExtra(TOKEN_AUTOPOPULATE_NAME); getIntent().removeExtra(TOKEN_AUTOPOPULATE_NAME);
} }
tagName.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
tagError.setVisibility(clashes() ? View.VISIBLE : View.GONE);
}
});
}
private String getNewName() {
return tagName.getText().toString().trim();
}
private boolean clashes() {
String newName = getNewName();
TagData existing = tagDataDao.getTagByName(newName, TagData.PROPERTIES);
return existing != null && tagData.getId() != existing.getId();
} }
private void save() { private void save() {
String oldName = tagData.getName(); String oldName = tagData.getName();
String newName = tagName.getText().toString().trim(); String newName = getNewName();
if (isEmpty(newName)) { if (isEmpty(newName)) {
return; return;
} }
boolean nameChanged = !oldName.equals(newName); if (clashes()) {
if (nameChanged) { return;
if (oldName.equalsIgnoreCase(newName)) { // Change the capitalization of a list manually }
if (isNewTag) {
tagData.setName(newName); tagData.setName(newName);
tagService.rename(tagData.getUuid(), newName); tagDataDao.persist(tagData);
} else { // Rename list--check for existing name setResult(RESULT_OK, new Intent().putExtra(TOKEN_NEW_FILTER, TagFilterExposer.filterFromTagData(TagSettingsActivity.this, tagData)));
newName = tagService.getTagWithCase(newName); } else if (!oldName.equals(newName)) {
tagName.setText(newName);
if (!newName.equals(oldName)) {
tagData.setName(newName); tagData.setName(newName);
tagService.rename(tagData.getUuid(), newName); tagService.rename(tagData.getUuid(), newName);
}
}
tagDataDao.persist(tagData); tagDataDao.persist(tagData);
if (isNewTag) {
setResult(RESULT_OK, new Intent().putExtra(TOKEN_NEW_FILTER,
TagFilterExposer.filterFromTagData(TagSettingsActivity.this, tagData)));
} else {
setResult(RESULT_OK, new Intent(AstridApiConstants.BROADCAST_EVENT_TAG_RENAMED).putExtra(TagViewFragment.EXTRA_TAG_UUID, tagData.getUuid())); setResult(RESULT_OK, new Intent(AstridApiConstants.BROADCAST_EVENT_TAG_RENAMED).putExtra(TagViewFragment.EXTRA_TAG_UUID, tagData.getUuid()));
} }
}
finish(); finish();
} }
@ -185,7 +206,7 @@ public class TagSettingsActivity extends InjectingAppCompatActivity {
} }
private void discard() { private void discard() {
String tagName = this.tagName.getText().toString().trim(); String tagName = getNewName();
if ((isNewTag && isEmpty(tagName)) || if ((isNewTag && isEmpty(tagName)) ||
(!isNewTag && tagData.getName().equals(tagName))) { (!isNewTag && tagData.getName().equals(tagName))) {
finish(); finish();

@ -20,8 +20,12 @@ import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
import com.google.api.client.repackaged.com.google.common.base.Strings;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.todoroo.andlib.data.AbstractModel; import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.Callback;
import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
@ -38,12 +42,18 @@ import org.tasks.dialogs.DialogBuilder;
import org.tasks.preferences.ActivityPreferences; import org.tasks.preferences.ActivityPreferences;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.newHashSet;
/** /**
* Control set to manage adding and removing tags * Control set to manage adding and removing tags
* *
@ -61,7 +71,6 @@ public final class TagsControlSet extends PopupControlSet {
private LinearLayout newTags; private LinearLayout newTags;
private ListView selectedTags; private ListView selectedTags;
private boolean populated = false; private boolean populated = false;
private HashMap<String, Integer> tagIndices;
//private final LinearLayout tagsContainer; //private final LinearLayout tagsContainer;
private final TextView tagsDisplay; private final TextView tagsDisplay;
@ -78,34 +87,12 @@ public final class TagsControlSet extends PopupControlSet {
tagsDisplay = (TextView) getView().findViewById(R.id.display_row_edit); tagsDisplay = (TextView) getView().findViewById(R.id.display_row_edit);
} }
private TagData[] getTagArray() {
List<TagData> tagList = tagService.getTagList();
return tagList.toArray(new TagData[tagList.size()]);
}
private HashMap<String, Integer> buildTagIndices(ArrayList<String> tagNames) {
HashMap<String, Integer> indices = new HashMap<>();
for (int i = 0; i < tagNames.size(); i++) {
indices.put(tagNames.get(i), i);
}
return indices;
}
private ArrayList<String> getTagNames(TagData[] tags) {
ArrayList<String> names = new ArrayList<>();
for (TagData tag : tags) {
if (!names.contains(tag.getName())) {
names.add(tag.getName());
}
}
return names;
}
private String buildTagString() { private String buildTagString() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
LinkedHashSet<String> tags = getTagSet(); List<String> tagList = getTagList();
for (String tag : tags) { Collections.sort(tagList);
for (String tag : tagList) {
if (tag.trim().length() == 0) { if (tag.trim().length() == 0) {
continue; continue;
} }
@ -118,41 +105,50 @@ public final class TagsControlSet extends PopupControlSet {
return builder.toString(); return builder.toString();
} }
private void setTagSelected(String tag) { private void setTagSelected(String tag) {
Integer index = tagIndices.get(tag); int index = allTagNames.indexOf(tag);
if (index != null) { if (index >= 0) {
selectedTags.setItemChecked(index, true); selectedTags.setItemChecked(index, true);
} else { } else {
allTagNames.add(tag); allTagNames.add(tag);
tagIndices.put(tag, allTagNames.size() - 1);
((ArrayAdapter<String>)selectedTags.getAdapter()).notifyDataSetChanged(); ((ArrayAdapter<String>)selectedTags.getAdapter()).notifyDataSetChanged();
} }
} }
private LinkedHashSet<String> getTagSet() { private List<String> getTagList() {
LinkedHashSet<String> tags = new LinkedHashSet<>(); Set<String> tags = new LinkedHashSet<>();
if (initialized) { if (initialized) {
for(int i = 0; i < selectedTags.getAdapter().getCount(); i++) { for(int i = 0; i < selectedTags.getAdapter().getCount(); i++) {
if (selectedTags.isItemChecked(i)) { if (selectedTags.isItemChecked(i)) {
tags.add(allTagNames.get(i)); tags.add(allTagNames.get(i));
} }
} }
for (int i = newTags.getChildCount() - 1 ; i >= 0 ; i--) {
for(int i = 0; i < newTags.getChildCount(); i++) {
TextView tagName = (TextView) newTags.getChildAt(i).findViewById(R.id.text1); TextView tagName = (TextView) newTags.getChildAt(i).findViewById(R.id.text1);
if(tagName.getText().length() == 0) { final String text = tagName.getText().toString();
if (Strings.isNullOrEmpty(text)) {
continue; continue;
} }
TagData tagByName = tagDataDao.getTagByName(text, TagData.PROPERTIES);
tags.add(tagName.getText().toString()); if (tagByName != null) {
setTagSelected(tagByName.getName());
tags.add(tagByName.getName());
newTags.removeViewAt(i);
} else if (!Iterables.any(tags, new Predicate<String>() {
@Override
public boolean apply(@Nullable String input) {
return text.equalsIgnoreCase(input);
}
})) {
tags.add(text);
}
} }
} else { } else {
if (model.getTransitory(TRANSITORY_TAGS) != null) { if (model.getTransitory(TRANSITORY_TAGS) != null) {
return (LinkedHashSet<String>) model.getTransitory(TRANSITORY_TAGS); return (List<String>) model.getTransitory(TRANSITORY_TAGS);
} }
} }
return tags; return newArrayList(tags);
} }
/** Adds a tag to the tag field */ /** Adds a tag to the tag field */
@ -245,7 +241,7 @@ public final class TagsControlSet extends PopupControlSet {
public void readFromTask(Task task) { public void readFromTask(Task task) {
super.readFromTask(task); super.readFromTask(task);
if(model.getId() != AbstractModel.NO_ID) { if(model.getId() != AbstractModel.NO_ID) {
model.putTransitory(TRANSITORY_TAGS, new LinkedHashSet<>(tagService.getTagNames(model.getId()))); model.putTransitory(TRANSITORY_TAGS, tagService.getTagNames(model.getId()));
refreshDisplayView(); refreshDisplayView();
} }
} }
@ -271,7 +267,7 @@ public final class TagsControlSet extends PopupControlSet {
} }
private void selectTagsFromModel() { private void selectTagsFromModel() {
LinkedHashSet<String> tags = (LinkedHashSet<String>) model.getTransitory(TRANSITORY_TAGS); List<String> tags = (List<String>) model.getTransitory(TRANSITORY_TAGS);
if (tags != null) { if (tags != null) {
for (String tag : tags) { for (String tag : tags) {
setTagSelected(tag); setTagSelected(tag);
@ -281,16 +277,18 @@ public final class TagsControlSet extends PopupControlSet {
@Override @Override
protected void afterInflate() { protected void afterInflate() {
TagData[] allTags = getTagArray(); allTagNames = newArrayList(ImmutableSet.copyOf(transform(tagService.getTagList(), new Function<TagData, String>() {
allTagNames = getTagNames(allTags); @Override
tagIndices = buildTagIndices(allTagNames); public String apply(TagData tagData) {
return tagData.getName();
}
})));
selectedTags = (ListView) getDialogView().findViewById(R.id.existingTags); selectedTags = (ListView) getDialogView().findViewById(R.id.existingTags);
selectedTags.setAdapter(new ArrayAdapter<>(activity, selectedTags.setAdapter(new ArrayAdapter<>(activity,
R.layout.simple_list_item_multiple_choice_themed, allTagNames)); R.layout.simple_list_item_multiple_choice_themed, allTagNames));
selectedTags.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); selectedTags.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
newTags = (LinearLayout) getDialogView().findViewById(R.id.newTags);
this.newTags = (LinearLayout) getDialogView().findViewById(R.id.newTags);
} }
@Override @Override
@ -300,9 +298,7 @@ public final class TagsControlSet extends PopupControlSet {
return; return;
} }
LinkedHashSet<String> tags = getTagSet(); synchronizeTags(task.getId(), task.getUUID());
synchronizeTags(task.getId(), task.getUUID(), tags);
Flags.set(Flags.TAGS_CHANGED); Flags.set(Flags.TAGS_CHANGED);
task.setModificationDate(DateUtilities.now()); task.setModificationDate(DateUtilities.now());
} }
@ -322,50 +318,55 @@ public final class TagsControlSet extends PopupControlSet {
/** /**
* Save the given array of tags into the database * Save the given array of tags into the database
*/ */
private void synchronizeTags(long taskId, String taskUuid, Set<String> tags) { private void synchronizeTags(long taskId, String taskUuid) {
Query query = Query.select(Metadata.PROPERTIES).where( Query query = Query.select(Metadata.PROPERTIES).where(
Criterion.and( Criterion.and(
TaskToTagMetadata.TASK_UUID.eq(taskUuid), TaskToTagMetadata.TASK_UUID.eq(taskUuid),
Metadata.DELETION_DATE.eq(0)) Metadata.DELETION_DATE.eq(0))
); );
final HashSet<String> existingLinks = new HashSet<>(); Set<String> existingTagIds = newHashSet(transform(metadataDao.toList(query), new Function<Metadata, String>() {
metadataDao.query(query, new Callback<Metadata>() {
@Override @Override
public void apply(Metadata link) { public String apply(Metadata tagData) {
existingLinks.add(link.getValue(TaskToTagMetadata.TAG_UUID)); return tagData.getValue(TaskToTagMetadata.TAG_UUID);
} }
}); }));
List<String> tags = getTagList();
// create missing tags
for (String tag : tags) { for (String tag : tags) {
if (tag.trim().length() == 0) { TagData tagData = tagDataDao.getTagByName(tag, TagData.ID);
continue;
}
TagData tagData = tagDataDao.getTagByName(tag, TagData.NAME, TagData.UUID);
if (tagData == null) { if (tagData == null) {
tagData = new TagData(); tagData = new TagData();
tagData.setName(tag); tagData.setName(tag);
tagDataDao.persist(tagData); tagDataDao.persist(tagData);
} }
if (existingLinks.contains(tagData.getUUID())) { }
existingLinks.remove(tagData.getUUID()); List<TagData> selectedTags = newArrayList(transform(tags, new Function<String, TagData>() {
} else { @Override
Metadata newLink = TaskToTagMetadata.newTagMetadata(taskId, taskUuid, tag, tagData.getUUID()); public TagData apply(String tag) {
return tagDataDao.getTagByName(tag, TagData.PROPERTIES);
}
}));
deleteLinks(taskId, taskUuid, difference(existingTagIds, newHashSet(Iterables.transform(selectedTags, new Function<TagData, String>() {
@Override
public String apply(TagData tagData) {
return tagData.getUUID();
}
}))));
for (TagData tagData : selectedTags) {
if (!existingTagIds.contains(tagData.getUUID())) {
Metadata newLink = TaskToTagMetadata.newTagMetadata(taskId, taskUuid, tagData.getName(), tagData.getUUID());
metadataDao.createNew(newLink); metadataDao.createNew(newLink);
} }
} }
// Mark as deleted links that don't exist anymore
deleteLinks(taskId, taskUuid, existingLinks.toArray(new String[existingLinks.size()]));
} }
/** /**
* Delete all links between the specified task and the list of tags * Delete all links between the specified task and the list of tags
*/ */
private void deleteLinks(long taskId, String taskUuid, String[] tagUuids) { private void deleteLinks(long taskId, String taskUuid, Iterable<String> tagUuids) {
Metadata deleteTemplate = new Metadata(); Metadata deleteTemplate = new Metadata();
deleteTemplate.setTask(taskId); // Need this for recording changes in outstanding table deleteTemplate.setTask(taskId); // Need this for recording changes in outstanding table
deleteTemplate.setDeletionDate(DateUtilities.now()); deleteTemplate.setDeletionDate(DateUtilities.now());
if (tagUuids != null) {
for (String uuid : tagUuids) { for (String uuid : tagUuids) {
// TODO: Right now this is in a loop because each deleteTemplate needs the individual tagUuid in order to record // TODO: Right now this is in a loop because each deleteTemplate needs the individual tagUuid in order to record
// the outstanding entry correctly. If possible, this should be improved to a single query // the outstanding entry correctly. If possible, this should be improved to a single query
@ -374,5 +375,4 @@ public final class TagsControlSet extends PopupControlSet {
TaskToTagMetadata.TASK_UUID.eq(taskUuid), TaskToTagMetadata.TAG_UUID.eq(uuid)), deleteTemplate); TaskToTagMetadata.TASK_UUID.eq(taskUuid), TaskToTagMetadata.TAG_UUID.eq(uuid)), deleteTemplate);
} }
} }
}
} }

@ -45,6 +45,19 @@
android:textColor="?attr/asTextColorHint" android:textColor="?attr/asTextColorHint"
android:textColorHint="?attr/asTextColorHint" android:textColorHint="?attr/asTextColorHint"
android:textSize="15sp" /> android:textSize="15sp" />
<TextView
android:id="@+id/tag_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@id/tag_name"
android:layout_marginTop="16dp"
android:text="@string/tag_already_exists"
android:textColor="@color/overdue"
android:textStyle="italic"
android:visibility="gone" />
</RelativeLayout> </RelativeLayout>
</ScrollView> </ScrollView>
</FrameLayout> </FrameLayout>

@ -137,6 +137,7 @@
<string name="send_anonymous_statistics_summary">Send anonymous usage statistics and crash reports to help improve Tasks. No personal data will be collected.</string> <string name="send_anonymous_statistics_summary">Send anonymous usage statistics and crash reports to help improve Tasks. No personal data will be collected.</string>
<string name="anonymous_usage_blurb">Anonymous usage statistics are collected</string> <string name="anonymous_usage_blurb">Anonymous usage statistics are collected</string>
<string name="opt_out">Opt-out</string> <string name="opt_out">Opt-out</string>
<string name="tag_already_exists">Tag already exists</string>
<string-array name="sync_SPr_interval_entries"> <string-array name="sync_SPr_interval_entries">
<!-- sync_SPr_interval_entries: Synchronization Intervals --> <!-- sync_SPr_interval_entries: Synchronization Intervals -->

Loading…
Cancel
Save