package com.orgzly.android.provider.clients; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.os.RemoteException; import android.support.v4.content.CursorLoader; import android.text.TextUtils; import com.orgzly.BuildConfig; import com.orgzly.R; import com.orgzly.android.Note; import com.orgzly.android.NotePosition; import com.orgzly.android.NotesBatch; import com.orgzly.android.SearchQuery; import com.orgzly.android.provider.DatabaseUtils; import com.orgzly.android.provider.GenericDatabaseUtils; import com.orgzly.android.provider.ProviderContract; import com.orgzly.android.provider.models.DbNote; import com.orgzly.android.ui.NotePlace; import com.orgzly.android.ui.NoteStateSpinner; import com.orgzly.android.ui.Place; import com.orgzly.android.util.LogUtils; import com.orgzly.android.util.MiscUtils; import com.orgzly.android.widgets.ListWidgetProvider; import com.orgzly.org.OrgHead; import com.orgzly.org.OrgProperty; import com.orgzly.org.datetime.OrgDateTime; import com.orgzly.org.datetime.OrgRange; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; public class NotesClient { private static final String TAG = NotesClient.class.getName(); public interface NotesClientInterface { void onNote(Note note); } public static void forEachBookNote(Context context, String bookName, NotesClientInterface notesClientInterface) { Cursor cursor = NotesClient.getCursorForBook(context, bookName); try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { Note note = NotesClient.fromCursor(cursor); List<OrgProperty> propertiesFromCursor = getNoteProperties(context, note.getId()); note.getHead().setProperties(propertiesFromCursor); notesClientInterface.onNote(note); } } finally { cursor.close(); } } public static List<OrgProperty> getNoteProperties(Context context, long noteId) { List<OrgProperty> properties = new ArrayList<>(); Cursor cursor = context.getContentResolver().query( ProviderContract.NoteProperties.ContentUri.notesIdProperties(noteId), null, null, null, null); try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { properties.add(new OrgProperty( cursor.getString(0), cursor.getString(1) )); } } finally { cursor.close(); } return properties; } public static void toContentValues(ContentValues values, Note note) { values.put(ProviderContract.Notes.UpdateParam.BOOK_ID, note.getPosition().getBookId()); values.put(ProviderContract.Notes.UpdateParam.LFT, note.getPosition().getLft()); values.put(ProviderContract.Notes.UpdateParam.RGT, note.getPosition().getRgt()); values.put(ProviderContract.Notes.UpdateParam.LEVEL, note.getPosition().getLevel()); values.put(ProviderContract.Notes.UpdateParam.IS_FOLDED, note.getPosition().isFolded()); values.put(ProviderContract.Notes.UpdateParam.DESCENDANTS_COUNT, note.getPosition().getDescendantsCount()); values.put(ProviderContract.Notes.UpdateParam.FOLDED_UNDER_ID, note.getPosition().getFoldedUnderId()); values.put(ProviderContract.Notes.UpdateParam.POSITION, 0); // TODO: Remove toContentValues(values, note.getHead()); } private static void toContentValues(ContentValues values, OrgHead head) { values.put(ProviderContract.Notes.UpdateParam.TITLE, head.getTitle()); if (head.hasScheduled()) { values.put(ProviderContract.Notes.UpdateParam.SCHEDULED_STRING, head.getScheduled().toString()); } else { values.putNull(ProviderContract.Notes.UpdateParam.SCHEDULED_STRING); } if (head.hasClosed()) { values.put(ProviderContract.Notes.UpdateParam.CLOSED_STRING, head.getClosed().toString()); } else { values.putNull(ProviderContract.Notes.UpdateParam.CLOSED_STRING); } if (head.hasClock()) { values.put(ProviderContract.Notes.UpdateParam.CLOCK_STRING, head.getClock().toString()); } else { values.putNull(ProviderContract.Notes.UpdateParam.CLOCK_STRING); } if (head.hasDeadline()) { values.put(ProviderContract.Notes.UpdateParam.DEADLINE_STRING, head.getDeadline().toString()); } else { values.putNull(ProviderContract.Notes.UpdateParam.DEADLINE_STRING); } values.put(ProviderContract.Notes.UpdateParam.PRIORITY, head.getPriority()); values.put(ProviderContract.Notes.UpdateParam.STATE, head.getState()); if (head.hasTags()) { values.put(ProviderContract.Notes.UpdateParam.TAGS, DbNote.dbSerializeTags(head.getTags())); } else { values.putNull(ProviderContract.Notes.UpdateParam.TAGS); } if (head.hasContent()) { values.put(ProviderContract.Notes.UpdateParam.CONTENT, head.getContent()); values.put(ProviderContract.Notes.UpdateParam.CONTENT_LINE_COUNT, MiscUtils.lineCount(head.getContent())); } else { values.putNull(ProviderContract.Notes.UpdateParam.CONTENT); values.put(ProviderContract.Notes.UpdateParam.CONTENT_LINE_COUNT, 0); } } public static Note fromCursor(Cursor cursor) { long id = idFromCursor(cursor); boolean isFolded = cursor.getInt(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.IS_FOLDED)) != 0; int contentLines = cursor.getInt(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.CONTENT_LINE_COUNT)); OrgHead head = headFromCursor(cursor); NotePosition position = DbNote.positionFromCursor(cursor); Note note = new Note(); note.setHead(head); note.setId(id); note.setPosition(position); note.setContentLines(contentLines); String inheritedTags = cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.INHERITED_TAGS)); if (! TextUtils.isEmpty(inheritedTags)) { note.setInheritedTags(DbNote.dbDeSerializeTags(inheritedTags)); } return note; } public static long idFromCursor(Cursor cursor) { return cursor.getLong(cursor.getColumnIndex(ProviderContract.Notes.QueryParam._ID)); } private static OrgHead headFromCursor(Cursor cursor) { OrgHead head = new OrgHead(); String state = cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.STATE)); if (NoteStateSpinner.isSet(state)) { head.setState(state); } else { head.setState(null); } String priority = cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.PRIORITY)); if (priority != null) { head.setPriority(priority); } head.setTitle(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.TITLE))); head.setContent(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.CONTENT))); if (! TextUtils.isEmpty(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.SCHEDULED_RANGE_STRING)))) head.setScheduled(OrgRange.getInstance(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.SCHEDULED_RANGE_STRING)))); if (! TextUtils.isEmpty(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.DEADLINE_RANGE_STRING)))) head.setDeadline(OrgRange.getInstance(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.DEADLINE_RANGE_STRING)))); if (! TextUtils.isEmpty(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.CLOSED_RANGE_STRING)))) head.setClosed(OrgRange.getInstance(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.CLOSED_RANGE_STRING)))); if (! TextUtils.isEmpty(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.CLOCK_RANGE_STRING)))) head.setClock(OrgRange.getInstance(cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.CLOCK_RANGE_STRING)))); // TODO: This is probably slowing UI down when scrolling fast, use strings from db directly? String tags = cursor.getString(cursor.getColumnIndex(ProviderContract.Notes.QueryParam.TAGS)); if (! TextUtils.isEmpty(tags)) { head.setTags(DbNote.dbDeSerializeTags(tags)); } return head; } /** * Updates note by its ID. */ public static int update(Context context, Note note) { ContentValues values = new ContentValues(); toContentValues(values, note.getHead()); Uri noteUri = ContentUris.withAppendedId(ProviderContract.Notes.ContentUri.notes(), note.getId()); Uri uri = noteUri.buildUpon().appendQueryParameter("bookId", String.valueOf(note.getPosition().getBookId())).build(); ArrayList<ContentProviderOperation> ops = new ArrayList<>(); /* Update note. */ ops.add(ContentProviderOperation .newUpdate(uri) .withValues(values) .build() ); /* Delete all note's properties. */ ops.add(ContentProviderOperation .newDelete(ProviderContract.NoteProperties.ContentUri.notesIdProperties(note.getId())) .build() ); /* Add each of the note's property. */ int i = 0; for (OrgProperty property: note.getHead().getProperties()) { values = new ContentValues(); values.put(ProviderContract.NoteProperties.Param.NOTE_ID, note.getId()); values.put(ProviderContract.NoteProperties.Param.NAME, property.getName()); values.put(ProviderContract.NoteProperties.Param.VALUE, property.getValue()); values.put(ProviderContract.NoteProperties.Param.POSITION, i++); ops.add(ContentProviderOperation .newInsert(ProviderContract.NoteProperties.ContentUri.notesProperties()) .withValues(values) .build() ); } ContentProviderResult[] result; try { result = context.getContentResolver().applyBatch(ProviderContract.AUTHORITY, ops); } catch (RemoteException | OperationApplicationException e) { e.printStackTrace(); throw new RuntimeException(e); } return result[0].count; } /** * Insert as last note if position is not specified. */ public static Note create(Context context, Note note, NotePlace target) { ContentValues values = new ContentValues(); toContentValues(values, note); Uri insertUri; if (target != null) { /* Create note relative to an existing note. */ insertUri = ProviderContract.Notes.ContentUri.notesIdTarget(target); } else { /* Create as last note. */ insertUri = ProviderContract.Notes.ContentUri.notes(); } ArrayList<ContentProviderOperation> ops = new ArrayList<>(); /* Insert note. */ ops.add(ContentProviderOperation .newInsert(insertUri) .withValues(values) .build() ); /* Add each of the note's property. */ int i = 0; for (OrgProperty property: note.getHead().getProperties()) { values = new ContentValues(); values.put(ProviderContract.NoteProperties.Param.NAME, property.getName()); values.put(ProviderContract.NoteProperties.Param.VALUE, property.getValue()); values.put(ProviderContract.NoteProperties.Param.POSITION, i++); ops.add(ContentProviderOperation .newInsert(ProviderContract.NoteProperties.ContentUri.notesProperties()) .withValues(values) .withValueBackReference(ProviderContract.NoteProperties.Param.NOTE_ID, 0) .build() ); } ContentProviderResult[] result; try { result = context.getContentResolver().applyBatch(ProviderContract.AUTHORITY, ops); } catch (RemoteException | OperationApplicationException e) { e.printStackTrace(); throw new RuntimeException(e); } long noteId = ContentUris.parseId(result[0].uri); /* Update ID of newly inserted note. */ note.setId(noteId); return note; } /** * Deletes all notes belonging to specified book. */ public static void deleteFromBook(Context context, long bookId) { int deleted = context.getContentResolver().delete(ProviderContract.Notes.ContentUri.notes(), ProviderContract.Notes.UpdateParam.BOOK_ID + "=" + bookId, null); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Deleted all (" + deleted + ") notes from book " + bookId); } public static int delete(Context context, long[] noteIds) { int deleted = 0; ArrayList<ContentProviderOperation> ops = new ArrayList<>(); for (long noteId: noteIds) { ops.add(ContentProviderOperation .newDelete(ProviderContract.Notes.ContentUri.notes()) .withSelection(ProviderContract.Notes.UpdateParam._ID + "=" + noteId, null) .build() ); } try { context.getContentResolver().applyBatch(ProviderContract.AUTHORITY, ops); } catch (RemoteException | OperationApplicationException e) { e.printStackTrace(); throw new RuntimeException(e); } if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Deleted " + deleted + " notes"); return deleted; } /** * Pastes back the latest cut batch. * @return number of notes restored * * Restores all notes cut with specified batch (see cut()) * @return number of notes restored */ // public int undoCut() { // NotesBatch batch = getLatestNotesBatch(); // // if (batch == null) { // No cut notes. // return 0; // } // // if (batch.getId() <= 0) { // throw new IllegalArgumentException("Paste batch id (" + batch + ") must be greater then 0"); // } // // if (batch.getId() > System.currentTimeMillis()) { // throw new IllegalArgumentException("Paste batch id (" + batch + ") must be less then current time"); // } // // int pasted = 0; // // ContentValues values = new ContentValues(); // values.put(Contract.Notes.Column.IS_CUT, 0); // // pasted += context.getContentResolver().update(Contract.Notes.CONTENT_URI, values, Contract.Notes.Column.IS_CUT + "=" + batch.getId(), null); // // if (BuildConfig.LOG_DEBUG) Dlog.d(TAG, "Pasted " + pasted + " notes with " + batch); // // // return pasted; // } // TODO: Don't throw Exception, return null? public static Note getNote(Context context, long noteId) { Cursor cursor = context.getContentResolver().query(ProviderContract.Notes.ContentUri.notes(), null, ProviderContract.Notes.QueryParam._ID + "=" + noteId, null, null); // TODO: Do not select all columns, especially not content if not required. try { if (cursor.moveToFirst()) { return fromCursor(cursor); } else { throw new NoSuchElementException("Note with id " + noteId + " was not found in " + ProviderContract.Notes.ContentUri.notes()); } } finally { cursor.close(); } } /** * Returns first note that matches the title. * Currently used only by tests -- title is not unique and notebook ID is not even specified. */ public static Note getNote(Context context, String title) { Cursor cursor = context.getContentResolver().query( ProviderContract.Notes.ContentUri.notes(), null, ProviderContract.Notes.QueryParam.TITLE + "= ?", new String[] { title }, null); try { if (cursor.moveToFirst()) { return fromCursor(cursor); } else { throw new NoSuchElementException("Note with title " + title + " was not found in " + ProviderContract.Notes.ContentUri.notes()); } } finally { cursor.close(); } } public static Cursor getCursorForBook(Context context, String bookName) throws SQLException { SearchQuery searchQuery = new SearchQuery(); if (bookName != null) { searchQuery.setBookName(bookName); } return context.getContentResolver().query( ProviderContract.Notes.ContentUri.notesSearchQueried(searchQuery), null, // TODO: Do not fetch content if it is not required, for speed. null, null, ProviderContract.Notes.QueryParam.LFT); /* For book, simply order by position. */ } public static CursorLoader getLoaderForQuery(Context context, SearchQuery searchQuery) throws SQLException { return new CursorLoader( context, ProviderContract.Notes.ContentUri.notesSearchQueried(searchQuery), null, // TODO: Do not fetch content if it is not required, for speed. null, null, getOrderForQuery(context, searchQuery)); } public static Cursor getCursorForQuery(Context context, SearchQuery searchQuery) throws SQLException { return context.getContentResolver().query( ProviderContract.Notes.ContentUri.notesSearchQueried(searchQuery), null, // TODO: Do not fetch content if it is not required, for speed. null, null, getOrderForQuery(context, searchQuery)); } /** * Determines order of notes depending on {@link SearchQuery}. */ private static String getOrderForQuery(Context context, SearchQuery searchQuery) { /* Get default priority from user settings. */ String defaultPriority = android.preference.PreferenceManager.getDefaultSharedPreferences(context).getString( context.getResources().getString(R.string.pref_key_default_priority), context.getResources().getString(R.string.pref_default_default_priority)); ArrayList<String> orderByColumns = new ArrayList<>(); /* If user-specified sort order exists, use only that. */ if (searchQuery.hasSortOrder()) { for (SearchQuery.SortOrder so: searchQuery.getSortOrder()) { if (so.getType() == SearchQuery.SortOrder.Type.NOTEBOOK) { orderByColumns.add(ProviderContract.Notes.QueryParam.BOOK_NAME + (so.isAscending() ? "" : " DESC")); } else if (so.getType() == SearchQuery.SortOrder.Type.SCHEDULED) { orderByColumns.add(ProviderContract.Notes.QueryParam.SCHEDULED_TIME_TIMESTAMP + " IS NULL"); orderByColumns.add(ProviderContract.Notes.QueryParam.SCHEDULED_TIME_TIMESTAMP + (so.isAscending()? "" : " DESC")); } else if (so.getType() == SearchQuery.SortOrder.Type.DEADLINE) { orderByColumns.add(ProviderContract.Notes.QueryParam.DEADLINE_TIME_TIMESTAMP + " IS NULL"); orderByColumns.add(ProviderContract.Notes.QueryParam.DEADLINE_TIME_TIMESTAMP + (so.isAscending() ? "" : " DESC")); } else if (so.getType() == SearchQuery.SortOrder.Type.PRIORITY) { orderByColumns.add("COALESCE(" + ProviderContract.Notes.QueryParam.PRIORITY + ", '" + defaultPriority + "')" + (so.isAscending() ? "" : " DESC")); } } } else { orderByColumns.add(ProviderContract.Notes.QueryParam.BOOK_NAME); /* Priority or default priority. */ orderByColumns.add("COALESCE(" + ProviderContract.Notes.QueryParam.PRIORITY + ", '" + defaultPriority + "')"); if (searchQuery.hasDeadline()) { orderByColumns.add(ProviderContract.Notes.QueryParam.DEADLINE_TIME_TIMESTAMP); } if (searchQuery.hasScheduled()) { orderByColumns.add(ProviderContract.Notes.QueryParam.SCHEDULED_TIME_TIMESTAMP); } } /* Always sort by position last. */ orderByColumns.add(ProviderContract.Notes.QueryParam.LFT); return TextUtils.join(", ", orderByColumns); } public static int getCount(Context context, Long bookId) { String selection; if (bookId != null) { selection = DatabaseUtils.whereUncutBookNotes(bookId); } else { selection = DatabaseUtils.WHERE_EXISTING_NOTES; } return GenericDatabaseUtils.getCount(context, ProviderContract.Notes.ContentUri.notes(), selection); } /** * Collects all known tags from database. * @return Array of all known tags */ public static String[] getAllTags(Context context, long bookId) { Set<String> result = new HashSet<>(); /* If book id is specified, return only tags from that book. */ String selection = null; if (bookId > 0) { selection = ProviderContract.Notes.QueryParam.BOOK_ID + " = " + bookId; } Cursor cursor = context.getContentResolver().query(ProviderContract.Notes.ContentUri.notes(), new String[] { "DISTINCT " + ProviderContract.Notes.QueryParam.TAGS }, selection, null, null); try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { String tags = cursor.getString(0); if (! TextUtils.isEmpty(tags)) { result.addAll(Arrays.asList(DbNote.dbDeSerializeTags(tags))); } } } finally { cursor.close(); } return result.toArray(new String[result.size()]); } /** * Get the id of the first note in book. * * Used only by tests. * * @return note id or 0 if none found */ public static long getFirstNoteId(Context context, long bookId) { Cursor cursor = context.getContentResolver().query( ProviderContract.Notes.ContentUri.notes(), DatabaseUtils.PROJECTION_FOR_ID, DatabaseUtils.whereUncutBookNotes(bookId), null, ProviderContract.Notes.QueryParam.LFT ); try { if (cursor.moveToFirst()) { return cursor.getLong(0); } else { // No records found. return 0; } } finally { cursor.close(); } } public static int cut(Context context, long bookId, Set<Long> noteIds) { ContentValues values = new ContentValues(); values.put(ProviderContract.Cut.Param.BOOK_ID, bookId); values.put(ProviderContract.Cut.Param.IDS, TextUtils.join(",", noteIds)); return context.getContentResolver().update(ProviderContract.Cut.ContentUri.cut(), values, null, null); } public static NotesBatch paste(Context context, long bookId, long noteId, Place place) { NotesBatch batch = getLatestNotesBatch(context); if (batch != null) { ContentValues values = new ContentValues(); values.put(ProviderContract.Paste.Param.SPOT, place.toString()); values.put(ProviderContract.Paste.Param.NOTE_ID, noteId); values.put(ProviderContract.Paste.Param.BATCH_ID, batch.getId()); context.getContentResolver().update(ProviderContract.Paste.ContentUri.paste(), values, null, null); } return batch; } public static int delete(Context context, long bookId, Set<Long> noteIds) { ContentValues values = new ContentValues(); values.put(ProviderContract.Delete.Param.BOOK_ID, bookId); values.put(ProviderContract.Delete.Param.IDS, TextUtils.join(",", noteIds)); return context.getContentResolver().update(ProviderContract.Delete.ContentUri.delete(), values, null, null); } /** * Collect all notes with latest (newest, largest) batch id. * @return Latest {@link NotesBatch} */ public static NotesBatch getLatestNotesBatch(Context context) { /* Get latest batch ID. */ long batchId; Cursor cursor = context.getContentResolver().query( ProviderContract.Notes.ContentUri.notes(), new String[] { "MAX(" + ProviderContract.Notes.QueryParam.IS_CUT + ")" }, null, null, null); try { if (!cursor.moveToFirst()) { return null; } batchId = cursor.getLong(0); } finally { cursor.close(); } if (batchId == 0) { return null; } cursor = context.getContentResolver().query( ProviderContract.Notes.ContentUri.notes(), DatabaseUtils.PROJECTION_FOR_ID, ProviderContract.Notes.QueryParam.IS_CUT + " = " + batchId, null, null); try { int count = cursor.getCount(); if (count == 0) { return null; } Set<Long> ids = new HashSet<>(); for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { ids.add(cursor.getLong(0)); } NotesBatch batch = new NotesBatch(batchId, ids); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Latest cut batch: " + batch); return batch; } finally { cursor.close(); } } public static void updateScheduledTime(Context context, Set<Long> noteIds, OrgDateTime time) { ArrayList<ContentProviderOperation> ops = new ArrayList<>(); String noteIdsCommaSeparated = TextUtils.join(",", noteIds); /* Update notes. */ ContentValues values = new ContentValues(); if (time != null) { values.put(ProviderContract.Notes.UpdateParam.SCHEDULED_STRING, OrgRange.getInstance(time).toString()); } else { values.putNull(ProviderContract.Notes.UpdateParam.SCHEDULED_STRING); } ops.add(ContentProviderOperation .newUpdate(ProviderContract.Notes.ContentUri.notes()) .withValues(values) .withSelection(ProviderContract.Notes.UpdateParam._ID + " IN (" + noteIdsCommaSeparated + ")", null) .build()); updateBooksMtimeForNotes(context, noteIdsCommaSeparated, ops); /* * Apply batch. */ try { context.getContentResolver().applyBatch(ProviderContract.AUTHORITY, ops); } catch (RemoteException | OperationApplicationException e) { e.printStackTrace(); throw new RuntimeException(e); } } /** * Update state of specified notes. */ public static void setState(Context context, Set<Long> noteIds, String state) { String noteIdsCommaSeparated = TextUtils.join(",", noteIds); ContentValues values = new ContentValues(); values.put(ProviderContract.NotesState.Param.NOTE_IDS, noteIdsCommaSeparated); /** * TODO: Do not update state in DB with NO_STATE_KEYWORD - that should be UI-only thing * Then stop checking for it in NoteStateSpinner.isSet */ values.put(ProviderContract.NotesState.Param.STATE, state != null ? state : NoteStateSpinner.NO_STATE_KEYWORD); context.getContentResolver().update(ProviderContract.NotesState.ContentUri.notesState(), values, null, null); /* Affected books' mtime will be modified in content provider. */ } /** * TODO: Add operation for updating books' mtime. * Make sure this is called after updating book's notes, as it will trigger book loader * which could load old notes if they were not already updated. */ private static void updateBooksMtimeForNotes(Context context, String noteIdsCommaSeparated, ArrayList<ContentProviderOperation> ops) { String bookIdsCommaSeparated = getBooksForNotes(context, noteIdsCommaSeparated); if (bookIdsCommaSeparated != null) { ContentValues values = new ContentValues(); values.put(ProviderContract.Books.Param.MTIME, System.currentTimeMillis()); ops.add(ContentProviderOperation .newUpdate(ProviderContract.Books.ContentUri.books()) .withValues(values) .withSelection(ProviderContract.Books.Param._ID + " IN (" + bookIdsCommaSeparated + ")", null) .build()); } } /** * Get comma-separated list of distinct book ids for specified notes. */ private static String getBooksForNotes(Context context, String noteIdsCommaSeparated) { Cursor cursor = context.getContentResolver().query( ProviderContract.Notes.ContentUri.notes(), new String[] { "GROUP_CONCAT(DISTINCT " + ProviderContract.Notes.QueryParam.BOOK_ID + ")" }, ProviderContract.Notes.QueryParam._ID + " IN (" + noteIdsCommaSeparated + ")", null, null); try { if (cursor.moveToFirst()) { return cursor.getString(0); } } finally { cursor.close(); } return null; } public static List<Long> getDescendantsIds(Context context, Note note) { List<Long> ids = new ArrayList<>(); Cursor cursor = context.getContentResolver().query( ProviderContract.Notes.ContentUri.notes(), new String[] { ProviderContract.Notes.QueryParam._ID }, DatabaseUtils.whereDescendants( note.getPosition().getBookId(), note.getPosition().getLft(), note.getPosition().getRgt()), null, null); try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { long id = cursor.getLong(0); ids.add(id); } } finally { cursor.close(); } return ids; } public static void toggleFoldedState(Context context, long noteId) { context.getContentResolver().update(ProviderContract.Notes.ContentUri.notesIdToggleFoldedState(noteId), null, null, null); } }