/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.dao; import java.util.ArrayList; import java.util.HashSet; import android.content.ContentValues; import android.database.Cursor; import com.todoroo.andlib.data.AbstractModel; import com.todoroo.andlib.data.DatabaseDao; import com.todoroo.andlib.data.Property; 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.Query; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.actfm.sync.ActFmSyncThread; import com.todoroo.astrid.actfm.sync.messages.ChangesHappened; import com.todoroo.astrid.actfm.sync.messages.NameMaps; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.OutstandingEntry; import com.todoroo.astrid.data.RemoteModel; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.TaskOutstanding; import com.todoroo.astrid.provider.Astrid2TaskProvider; import com.todoroo.astrid.service.StatisticsConstants; import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.tags.TaskToTagMetadata; import com.todoroo.astrid.utility.AstridPreferences; /** * Data Access layer for {@link Metadata}-related operations. * * @author Tim Su <tim@todoroo.com> * */ public class MetadataDao extends DatabaseDao<Metadata> { @Autowired private Database database; @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public MetadataDao() { super(Metadata.class); DependencyInjectionService.getInstance().inject(this); setDatabase(database); } // --- SQL clause generators /** * Generates SQL clauses */ public static class MetadataCriteria { /** Returns all metadata associated with a given task */ public static Criterion byTask(long taskId) { return Metadata.TASK.eq(taskId); } /** Returns all metadata associated with a given key */ public static Criterion withKey(String key) { return Metadata.KEY.eq(key); } /** Returns all metadata associated with a given key */ public static Criterion byTaskAndwithKey(long taskId, String key) { return Criterion.and(withKey(key), byTask(taskId)); } } @Override protected boolean shouldRecordOutstanding(Metadata item) { ContentValues cv = item.getSetValues(); return super.shouldRecordOutstanding(item) && cv != null && ((cv.containsKey(Metadata.KEY.name) && TaskToTagMetadata.KEY.equals(item.getValue(Metadata.KEY))) || (cv.containsKey(Metadata.DELETION_DATE.name) && item.getValue(Metadata.DELETION_DATE) > 0)) && RemoteModelDao.getOutstandingEntryFlag(RemoteModelDao.OUTSTANDING_ENTRY_FLAG_RECORD_OUTSTANDING); } @Override protected int createOutstandingEntries(long modelId, ContentValues modelSetValues) { Long taskId = modelSetValues.getAsLong(Metadata.TASK.name); String tagUuid = modelSetValues.getAsString(TaskToTagMetadata.TAG_UUID.name); Long deletionDate = modelSetValues.getAsLong(Metadata.DELETION_DATE.name); if (taskId == null || taskId == AbstractModel.NO_ID || RemoteModel.isUuidEmpty(tagUuid)) return -1; TaskOutstanding to = new TaskOutstanding(); to.setValue(OutstandingEntry.ENTITY_ID_PROPERTY, taskId); to.setValue(OutstandingEntry.CREATED_AT_PROPERTY, DateUtilities.now()); String addedOrRemoved = NameMaps.TAG_ADDED_COLUMN; if (deletionDate != null && deletionDate > 0) addedOrRemoved = NameMaps.TAG_REMOVED_COLUMN; to.setValue(OutstandingEntry.COLUMN_STRING_PROPERTY, addedOrRemoved); to.setValue(OutstandingEntry.VALUE_STRING_PROPERTY, tagUuid); database.insert(outstandingTable.name, null, to.getSetValues()); ActFmSyncThread.getInstance().enqueueMessage(new ChangesHappened<Task, TaskOutstanding>(taskId, Task.class, PluginServices.getTaskDao(), PluginServices.getTaskOutstandingDao()), null); return 1; } /** * Synchronize metadata for given task id. Deletes rows in database that * are not identical to those in the metadata list, creates rows that * have no match. * * @param taskId id of task to perform synchronization on * @param metadata list of new metadata items to save * @param metadataCriteria criteria to load data for comparison from metadata */ public void synchronizeMetadata(long taskId, ArrayList<Metadata> metadata, Criterion metadataCriteria) { HashSet<ContentValues> newMetadataValues = new HashSet<ContentValues>(); for(Metadata metadatum : metadata) { metadatum.setValue(Metadata.TASK, taskId); metadatum.clearValue(Metadata.ID); newMetadataValues.add(metadatum.getMergedValues()); } Metadata item = new Metadata(); TodorooCursor<Metadata> cursor = query(Query.select(Metadata.PROPERTIES).where(Criterion.and(MetadataCriteria.byTask(taskId), metadataCriteria))); try { // try to find matches within our metadata list for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { item.readFromCursor(cursor); long id = item.getId(); // clear item id when matching with incoming values item.clearValue(Metadata.ID); ContentValues itemMergedValues = item.getMergedValues(); if(newMetadataValues.contains(itemMergedValues)) { newMetadataValues.remove(itemMergedValues); continue; } // not matched. cut it delete(id); } } finally { cursor.close(); } // everything that remains shall be written for(ContentValues values : newMetadataValues) { item.clear(); item.mergeWith(values); persist(item); } } @Override public boolean persist(Metadata item) { if(!item.containsValue(Metadata.CREATION_DATE)) item.setValue(Metadata.CREATION_DATE, DateUtilities.now()); boolean state = super.persist(item); if(Preferences.getBoolean(AstridPreferences.P_FIRST_LIST, true)) { if (state && item.containsNonNullValue(Metadata.KEY) && item.getValue(Metadata.KEY).equals(TaskToTagMetadata.KEY)) { StatisticsService.reportEvent(StatisticsConstants.USER_FIRST_LIST); Preferences.setBoolean(AstridPreferences.P_FIRST_LIST, false); } } Astrid2TaskProvider.notifyDatabaseModification(); return state; } /** * Fetch all metadata that are unattached to the task * @param database * @param properties * @return */ public TodorooCursor<Metadata> fetchDangling(Property<?>... properties) { Query sql = Query.select(properties).from(Metadata.TABLE).join(Join.left(Task.TABLE, Metadata.TASK.eq(Task.ID))).where(Task.TITLE.isNull()); Cursor cursor = database.rawQuery(sql.toString(), null); return new TodorooCursor<Metadata>(cursor, properties); } public boolean taskIsInTag(String taskUuid, String tagUuid) { TodorooCursor<Metadata> cursor = query(Query.select(Metadata.ID).where(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TASK_UUID.eq(taskUuid), TaskToTagMetadata.TAG_UUID.eq(tagUuid), Metadata.DELETION_DATE.eq(0)))); try { return cursor.getCount() > 0; } finally { cursor.close(); } } }