package com.orgzly.android;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import com.orgzly.BuildConfig;
import com.orgzly.R;
import com.orgzly.android.prefs.AppPreferences;
import com.orgzly.android.provider.ProviderContract;
import com.orgzly.android.provider.clients.BooksClient;
import com.orgzly.android.provider.clients.CurrentRooksClient;
import com.orgzly.android.provider.clients.DbClient;
import com.orgzly.android.provider.clients.FiltersClient;
import com.orgzly.android.provider.clients.NotesClient;
import com.orgzly.android.provider.clients.ReposClient;
import com.orgzly.android.reminders.ReminderService;
import com.orgzly.android.repos.Repo;
import com.orgzly.android.repos.RepoFactory;
import com.orgzly.android.repos.Rook;
import com.orgzly.android.repos.VersionedRook;
import com.orgzly.android.sync.BookNamesake;
import com.orgzly.android.sync.BookSyncStatus;
import com.orgzly.android.ui.NotePlace;
import com.orgzly.android.ui.Place;
import com.orgzly.android.util.CircularArrayList;
import com.orgzly.android.util.LogUtils;
import com.orgzly.android.util.MiscUtils;
import com.orgzly.android.util.UriUtils;
import com.orgzly.org.OrgHead;
import com.orgzly.org.OrgProperty;
import com.orgzly.org.datetime.OrgDateTime;
import com.orgzly.org.parser.OrgParsedFile;
import com.orgzly.org.parser.OrgParser;
import com.orgzly.org.parser.OrgParserSettings;
import com.orgzly.org.parser.OrgParserWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Local books storage
*
* FIXME: Turned into much more over time.
*/
public class Shelf {
private static final String TAG = Shelf.class.getName();
private Context mContext;
private LocalStorage mLocalStorage;
public Shelf(Context context) {
mContext = context;
mLocalStorage = new LocalStorage(context);
}
public List<Book> getBooks() {
return BooksClient.getAll(mContext);
}
public Book getBook(long id) {
return BooksClient.get(mContext, id);
}
public Book getBook(String name) {
return BooksClient.get(mContext, name);
}
/**
* Creates a new empty book.
*
* @param name name of the book
*/
public Book createBook(String name) throws IOException {
return BooksClient.insert(mContext, new Book(name));
}
/**
* Creates new dummy book - temporary incomplete book
* (before remote book has been downloaded, or linked etc.)
*/
private Book createDummyBook(String name) throws IOException {
return BooksClient.insert(mContext, new Book(name, "", 0, true));
}
/**
* Loads book from resource.
*/
public Book loadBookFromResource(String name, BookName.Format format, Resources resources, int resId) throws IOException {
InputStream is = resources.openRawResource(resId);
try {
return loadBookFromStream(name, format, is);
} finally {
is.close();
}
}
public Book loadBookFromStream(String name, BookName.Format format, InputStream inputStream) throws IOException {
/* Save content to temporary file. */
File tmpFile = getTempBookFile();
try {
MiscUtils.writeStreamToFile(inputStream, tmpFile);
return loadBookFromFile(name, format, tmpFile);
} finally {
tmpFile.delete();
}
}
/**
* Imports {@code File} to database under specified book name.
* Overwrites existing book with the same name.
*/
// TODO: Separately update book here - we don't want to update its mtime if loaded from resource for example.
public Book loadBookFromFile(String name, BookName.Format format, File file) throws IOException {
return loadBookFromFile(name, format, file, null, null);
}
public Book loadBookFromFile(String name, BookName.Format format, File file, VersionedRook vrook) throws IOException {
return loadBookFromFile(name, format, file, vrook, null);
}
public Book loadBookFromFile(String name, BookName.Format format, File file, VersionedRook vrook, String selectedEncoding) throws IOException {
Uri uri = BooksClient.loadFromFile(mContext, name, format, file, vrook, selectedEncoding);
notifyDataChanged(mContext);
return BooksClient.get(mContext, ContentUris.parseId(uri));
}
public void setBookStatus(Book book, String status, BookAction action) {
BooksClient.updateStatus(mContext, book.getId(), status, action);
}
// TODO: Do in Provider under transaction
public void deleteBook(Book book, boolean deleteLinked) throws IOException {
if (deleteLinked) {
Repo repo = RepoFactory.getFromUri(mContext, book.getLink().getRepoUri());
if (repo != null) {
repo.delete(book.getLink().getUri());
}
}
NotesClient.deleteFromBook(mContext, book.getId());
BooksClient.delete(mContext, book.getId());
notifyDataChanged(mContext);
}
// TODO: This is used in tests, check if we are even deleting these books.
public File getTempBookFile() throws IOException {
return mLocalStorage.getTempBookFile();
}
/**
* Returns full string content of the book in format specified. Used by tests.
*/
public String getBookContent(String name, BookName.Format format) throws IOException {
Book book = getBook(name);
if (book != null) {
File file = getTempBookFile();
try {
writeBookToFile(book, format, file);
return MiscUtils.readStringFromFile(file);
} finally {
file.delete();
}
}
return null;
}
public File exportBook(long bookId, BookName.Format format) throws IOException {
/* Get book from database. */
Book book = getBook(bookId);
/* Get file to write book to. */
File file = mLocalStorage.getExportFile(book, format);
/* Write book. */
writeBookToFile(book, format, file);
return file;
}
/**
* Writes content of book from database to specified file.
* TODO: Do in Provider under transaction
*/
public void writeBookToFile(final Book book, BookName.Format format, File file) throws IOException {
/* Use the same encoding. */
String encoding = book.getUsedEncoding();
if (encoding == null) {
encoding = Charset.defaultCharset().name();
}
final PrintWriter out = new PrintWriter(file, encoding);
try {
String prefValue = AppPreferences.separateNotesWithNewLine(mContext);
OrgParserSettings parserSettings = OrgParserSettings.getBasic();
if (mContext.getString(R.string.pref_value_separate_notes_with_new_line_always).equals(prefValue)) {
parserSettings.separateNotesWithNewLine = OrgParserSettings.SeparateNotesWithNewLine.ALWAYS;
} else if (mContext.getString(R.string.pref_value_separate_notes_with_new_line_multi_line_notes_only).equals(prefValue)) {
parserSettings.separateNotesWithNewLine = OrgParserSettings.SeparateNotesWithNewLine.MULTI_LINE_NOTES_ONLY;
} else if (mContext.getString(R.string.pref_value_separate_notes_with_new_line_never).equals(prefValue)) {
parserSettings.separateNotesWithNewLine = OrgParserSettings.SeparateNotesWithNewLine.NEVER;
}
parserSettings.separateHeaderAndContentWithNewLine = AppPreferences.separateHeaderAndContentWithNewLine(mContext);
final OrgParserWriter parserWriter = new OrgParserWriter(parserSettings);
out.write(parserWriter.whiteSpacedFilePreface(book.getPreface()));
NotesClient.forEachBookNote(mContext, book.getName(), new NotesClient.NotesClientInterface() {
@Override
public void onNote(Note note) {
out.write(parserWriter.whiteSpacedHead(
note.getHead(),
note.getPosition().getLevel(),
book.getOrgFileSettings().isIndented()));
}
});
} finally {
out.close();
}
}
public void setNotesScheduledTime(Set<Long> noteIds, OrgDateTime time) {
NotesClient.updateScheduledTime(mContext, noteIds, time);
notifyDataChanged(mContext);
}
public void setNotesState(Set<Long> noteIds, String state) {
NotesClient.setState(mContext, noteIds, state);
notifyDataChanged(mContext);
}
public Note getNote(long id) {
return NotesClient.getNote(mContext, id);
}
public List<OrgProperty> getNoteProperties(long id) {
return NotesClient.getNoteProperties(mContext, id);
}
public Note getNote(String title) {
return NotesClient.getNote(mContext, title);
}
public int updateNote(Note note) {
int result = NotesClient.update(mContext, note);
notifyDataChanged(mContext);
return result;
}
public Note createNote(Note note, NotePlace target) {
/* Create new note. */
Note insertedNote = NotesClient.create(mContext, note, target);
BooksClient.setModifiedTime(mContext, note.getPosition().getBookId(), System.currentTimeMillis());
notifyDataChanged(mContext);
return insertedNote;
}
public String[] getAllTags(long bookId) {
return NotesClient.getAllTags(mContext, bookId);
}
public void toggleFoldedState(long noteId) {
NotesClient.toggleFoldedState(mContext, noteId);
}
public void promoteNotes(long bookId, Set<Long> noteIds) {
BooksClient.promote(mContext, bookId, noteIds);
}
public int promoteNotes(long bookId, long noteId) {
Set<Long> noteIds = new HashSet<>();
noteIds.add(noteId);
return BooksClient.promote(mContext, bookId, noteIds);
}
public void demoteNotes(long bookId, Set<Long> noteIds) {
BooksClient.demote(mContext, bookId, noteIds);
}
public int demoteNotes(long bookId, long noteId) {
Set<Long> noteIds = new HashSet<>();
noteIds.add(noteId);
return BooksClient.demote(mContext, bookId, noteIds);
}
public int cut(long bookId, long noteId) {
Set<Long> noteIds = new HashSet<>();
noteIds.add(noteId);
return cut(bookId, noteIds);
}
public int cut(long bookId, Set<Long> noteIds) {
int result = NotesClient.cut(mContext, bookId, noteIds);
notifyDataChanged(mContext);
return result;
}
public NotesBatch paste(long bookId, long noteId, Place place) {
NotesBatch batch = NotesClient.paste(mContext, bookId, noteId, place);
notifyDataChanged(mContext);
return batch;
}
public int delete(long bookId, Set<Long> noteIds) {
int result = NotesClient.delete(mContext, bookId, noteIds);
notifyDataChanged(mContext);
return result;
}
/**
* Recreate all tables.
*/
public void clearDatabase() {
DbClient.recreateTables(mContext);
/* Clear last sync time. */
AppPreferences.lastSuccessfulSyncTime(mContext, 0L);
notifyDataChanged(mContext);
}
/**
* Compares every local book with every remote one and calculates the status for each link.
*
* @return number of links (unique book names)
* @throws IOException
*/
public Map<String, BookNamesake> groupAllNotebooksByName() throws IOException {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Collecting all local and remote books ...");
Map<String, Repo> repos = ReposClient.getAll(mContext);
List<Book> localBooks = getBooks();
List<VersionedRook> versionedRooks = getBooksFromAllRepos(repos);
/* Group local and remote books by name. */
Map<String, BookNamesake> namesakes = BookNamesake.getAll(mContext, localBooks, versionedRooks);
/* If there is no local book, create empty "dummy" one. */
for (BookNamesake namesake : namesakes.values()) {
if (namesake.getBook() == null) {
Book book = createDummyBook(namesake.getName());
namesake.setBook(book);
}
namesake.updateStatus(repos.size());
}
return namesakes;
}
public Map<String, Repo> getAllRepos() {
return ReposClient.getAll(mContext);
}
/**
* Goes through each repository and collects all books from each one.
*/
public List<VersionedRook> getBooksFromAllRepos(Map<String, Repo> repos) throws IOException {
List<VersionedRook> result = new ArrayList<>();
if (repos == null) {
repos = getAllRepos();
}
for (Repo repo: repos.values()) { /* Each repository. */
List<VersionedRook> libBooks = repo.getBooks();
for (VersionedRook vrook : libBooks) { /* Each book in repository. */
result.add(vrook);
}
}
CurrentRooksClient.set(mContext, result);
return result;
}
/**
* Passed {@link com.orgzly.android.sync.BookNamesake} is NOT updated after load or save.
*
* FIXME: Hardcoded BookName.Format.ORG below
*/
public BookAction syncNamesake(final BookNamesake namesake) throws IOException {
String repoUrl;
String fileName;
BookAction bookAction = null;
switch (namesake.getStatus()) {
case NO_CHANGE:
bookAction = new BookAction(BookAction.Type.INFO, namesake.getStatus().msg());
break;
case BOOK_WITHOUT_LINK_AND_ONE_OR_MORE_ROOKS_EXIST:
case DUMMY_WITHOUT_LINK_AND_MULTIPLE_ROOKS:
case NO_BOOK_MULTIPLE_ROOKS:
case ONLY_BOOK_WITHOUT_LINK_AND_MULTIPLE_REPOS:
case BOOK_WITH_LINK_AND_ROOK_EXISTS_BUT_LINK_POINTING_TO_DIFFERENT_ROOK:
case CONFLICT_BOTH_BOOK_AND_ROOK_MODIFIED:
case CONFLICT_BOOK_WITH_LINK_AND_ROOK_BUT_NEVER_SYNCED_BEFORE:
case CONFLICT_LAST_SYNCED_ROOK_AND_LATEST_ROOK_ARE_DIFFERENT:
case ONLY_DUMMY:
bookAction = new BookAction(BookAction.Type.ERROR, namesake.getStatus().msg());
break;
/* Load remote book. */
case NO_BOOK_ONE_ROOK:
case DUMMY_WITHOUT_LINK_AND_ONE_ROOK:
loadBookFromRepo(namesake.getRooks().get(0));
bookAction = new BookAction(BookAction.Type.INFO,
namesake.getStatus().msg(UriUtils.friendlyUri(namesake.getRooks().get(0).getUri())));
break;
case BOOK_WITH_LINK_AND_ROOK_MODIFIED:
loadBookFromRepo(namesake.getLatestLinkedRook());
bookAction = new BookAction(BookAction.Type.INFO,
namesake.getStatus().msg(UriUtils.friendlyUri(namesake.getLatestLinkedRook().getUri())));
break;
case DUMMY_WITH_LINK:
loadBookFromRepo(namesake.getLatestLinkedRook());
bookAction = new BookAction(BookAction.Type.INFO,
namesake.getStatus().msg(UriUtils.friendlyUri(namesake.getLatestLinkedRook().getUri())));
break;
/* Save local book to repository. */
case ONLY_BOOK_WITHOUT_LINK_AND_ONE_REPO:
/* Save local book to the one and only repository. */
repoUrl = getAllRepos().entrySet().iterator().next().getValue().getUri().toString();
fileName = BookName.fileName(namesake.getBook().getName(), BookName.Format.ORG);
saveBookToRepo(repoUrl, fileName, namesake.getBook(), BookName.Format.ORG);
bookAction = new BookAction(BookAction.Type.INFO, namesake.getStatus().msg(UriUtils.friendlyUri(repoUrl)));
break;
case BOOK_WITH_LINK_LOCAL_MODIFIED:
repoUrl = namesake.getBook().getLastSyncedToRook().getRepoUri().toString();
fileName = BookName.getFileName(mContext, namesake.getBook().getLastSyncedToRook().getUri());
saveBookToRepo(repoUrl, fileName, namesake.getBook(), BookName.Format.ORG);
bookAction = new BookAction(BookAction.Type.INFO, namesake.getStatus().msg(UriUtils.friendlyUri(repoUrl)));
break;
case ONLY_BOOK_WITH_LINK:
repoUrl = namesake.getBook().getLink().getRepoUri().toString();
fileName = BookName.getFileName(mContext, namesake.getBook().getLink().getUri());
saveBookToRepo(repoUrl, fileName, namesake.getBook(), BookName.Format.ORG);
bookAction = new BookAction(BookAction.Type.INFO, namesake.getStatus().msg(UriUtils.friendlyUri(repoUrl)));
break;
}
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Syncing " + namesake + ": " + bookAction);
return bookAction;
}
/**
* Downloads remote book, parses it and stores it to {@link Shelf}.
* @return book now linked to remote one
* @throws IOException
*/
public Book loadBookFromRepo(Rook rook) throws IOException {
Book book;
Repo repo = RepoFactory.getFromUri(mContext, rook.getRepoUri());
if (repo == null) {
throw new IOException("Unsupported repository URL \"" + rook.getRepoUri() + "\"");
}
File tmpFile = getTempBookFile();
try {
/* Download from repo. */
VersionedRook vrook = repo.retrieveBook(rook.getUri(), tmpFile);
String fileName = BookName.getFileName(mContext, vrook.getUri());
BookName bookName = BookName.fromFileName(fileName);
/* Store from file to Shelf. */
book = loadBookFromFile(bookName.getName(), bookName.getFormat(), tmpFile, vrook);
} finally {
tmpFile.delete();
}
return book;
}
/**
* Exports {@code Book}, uploads it to repo and link it to newly created
* {@link com.orgzly.android.repos.VersionedRook}.
*
* @return {@link Book}
* @throws IOException
*/
public Book saveBookToRepo(String repoUrl, String fileName, Book book, BookName.Format format) throws IOException {
Repo repo = RepoFactory.getFromUri(mContext, repoUrl);
if (repo == null) {
throw new IOException("Unsupported repository URL \"" + repoUrl + "\"");
}
VersionedRook uploadedBook;
File tmpFile = getTempBookFile();
try {
/* Write to temporary file. */
writeBookToFile(book, format, tmpFile);
/* Upload to repo. */
uploadedBook = repo.storeBook(tmpFile, fileName);
} finally {
/* Delete temporary file. */
tmpFile.delete();
}
book.setLastSyncedToRook(uploadedBook);
BooksClient.saved(mContext, book.getId(), uploadedBook);
return book;
}
public int updateBookSettings(Book book) {
return BooksClient.updateSettings(mContext, book);
}
public void renameBook(Book book, String name) throws IOException {
String oldName = book.getName();
/* Make sure there is no notebook with this name. */
if (getBook(name) != null) {
throw new IOException("Notebook with that name already exists");
}
/* Make sure link's repo is the same as sync book repo. */
if (book.getLink() != null && book.getLastSyncedToRook() != null) {
if (! book.getLink().getUri().equals(book.getLastSyncedToRook().getUri())) {
String s = BookSyncStatus.ROOK_AND_VROOK_HAVE_DIFFERENT_REPOS.toString();
setBookStatus(book, s, new BookAction(BookAction.Type.ERROR, s));
return;
}
}
/* Do not rename if there are local changes. */
if (book.getLastSyncedToRook() != null) {
if (book.isModifiedAfterLastSync()) {
throw new IOException("Notebook is not synced");
}
}
/* Prefer link. */
if (book.getLastSyncedToRook() != null) {
VersionedRook vrook = book.getLastSyncedToRook();
Repo repo = RepoFactory.getFromUri(mContext, vrook.getRepoUri());
VersionedRook movedVrook = repo.renameBook(vrook.getUri(), name);
book.setLastSyncedToRook(movedVrook);
BooksClient.saved(mContext, book.getId(), movedVrook);
}
if (BooksClient.updateName(mContext, book.getId(), name) != 1) {
String msg = mContext.getString(R.string.failed_renaming_book);
setBookStatus(book, null, new BookAction(BookAction.Type.ERROR, msg));
throw new IOException(msg);
}
setBookStatus(book, null, new BookAction(BookAction.Type.INFO, mContext.getString(R.string.renamed_book_from, oldName)));
}
public Uri addRepoUrl(String url) {
return ReposClient.insert(mContext, url);
}
public int updateRepoUrl(long id, String url) {
return ReposClient.updateUrl(mContext, id, url);
}
public int deleteRepo(long id) {
return ReposClient.delete(mContext, id);
}
public void shiftState(long id, int direction) {
Note note = NotesClient.getNote(mContext, id);
shiftState(note, direction);
}
public void shiftState(Note note, int direction) {
String currentState = note.getHead().getState();
ArrayList<String> array = new ArrayList<>();
array.add(null);
array.addAll(AppPreferences.todoKeywordsSet(mContext));
array.addAll(AppPreferences.doneKeywordsSet(mContext));
CircularArrayList<String> allStates = new CircularArrayList<>(array.toArray(new String[array.size()]));
int currentIndex = allStates.indexOf(currentState);
int nextIndex = currentIndex + direction;
String nextState = allStates.get(nextIndex);
Set<Long> noteIds = new HashSet<>();
noteIds.add(note.getId());
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Setting state for " + note.getHead().getTitle() + " to " + nextState +
": " + currentIndex + " -> " + nextIndex + " (" + allStates.size() + " total states)");
setNotesState(noteIds, nextState);
}
public void deleteFilters(Set<Long> ids) {
// TODO: Send a single request. */
for (long id: ids) {
FiltersClient.delete(mContext, id);
}
}
public void createFilter(Filter filter) {
FiltersClient.create(mContext, filter);
}
public void updateFilter(long id, Filter filter) {
FiltersClient.update(mContext, id, filter);
}
public void moveFilterUp(long id) {
FiltersClient.moveUp(mContext, id);
}
public void moveFilterDown(long id) {
FiltersClient.moveDown(mContext, id);
}
public void cycleVisibility(Book book) {
BooksClient.cycleVisibility(mContext, book);
}
public void setLink(Book book, String repoUrl) {
if (repoUrl == null) {
BooksClient.removeLink(mContext, book.getId());
} else {
String fileName;
/* Use file name used in last sync, if last sync exists. */
if (book.getLastSyncedToRook() != null) {
fileName = BookName.getFileName(mContext, book.getLastSyncedToRook().getUri());
} else {
fileName = BookName.fileName(book.getName(), BookName.Format.ORG);
}
Uri rookUri = Uri.parse(repoUrl).buildUpon().appendPath(fileName).build();
BooksClient.setLink(mContext, book.getId(), repoUrl, rookUri.toString());
}
}
public void setStateToDone(long noteId) {
Set<String> doneStates = AppPreferences.doneKeywordsSet(mContext);
String firstState = doneStates.iterator().hasNext() ? doneStates.iterator().next() : null;
if (firstState != null) {
Set<Long> ids = new TreeSet<>();
ids.add(noteId);
setNotesState(ids, firstState);
}
}
public static void notifyDataChanged(Context context) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG);
ReminderService.notifyDataChanged(context);
context.sendBroadcast(new Intent(AppIntent.ACTION_LIST_WIDGET_UPDATE));
}
public interface ReParsingNotesListener {
void noteParsed(int current, int total, String msg);
}
/**
* Using current states configuration, update states and titles for all notes.
* Keywords that were part of the title can become states and vice versa.
* Affected books' mtime will *not* be updated.
*
* @return Number of modified notes.
*/
public int reParseNotesStateAndTitles(ReParsingNotesListener listener) throws IOException {
int modifiedNotesCount = 0;
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
/* Get all notes. */
Cursor cursor = mContext.getContentResolver().query(
ProviderContract.Notes.ContentUri.notes(), null, null, null, null);
try {
int current = 0;
int total = 0;
OrgParser.Builder parserBuilder = new OrgParser.Builder()
.setTodoKeywords(AppPreferences.todoKeywordsSet(mContext))
.setDoneKeywords(AppPreferences.doneKeywordsSet(mContext));
OrgParserWriter parserWriter = new OrgParserWriter();
/* Get total number of notes for displaying the stats. */
if (cursor.moveToFirst()) {
total = cursor.getCount();
}
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
current++;
/* Get current heading string. */
Note note = NotesClient.fromCursor(cursor);
/* Skip root node. */
if (note.getPosition().getLevel() == 0) {
continue;
}
OrgHead head = note.getHead();
String headString = parserWriter.whiteSpacedHead(head, note.getPosition().getLevel(), false);
/* Re-parse heading using current setting of keywords. */
OrgParsedFile file = parserBuilder
.setInput(headString)
.build()
.parse();
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Note " + note + " parsed has " + file.getHeadsInList().size() + " notes");
if (file.getHeadsInList().size() != 1) {
throw new IOException("Got " + file.getHeadsInList().size() + " notes after parsing \"" + headString + "\"");
}
OrgHead newHead = file.getHeadsInList().get(0).getHead();
/* Update if state, title or priority are different. */
if (! TextUtils.equals(newHead.getState(), head.getState()) ||
! TextUtils.equals(newHead.getTitle(), head.getTitle()) ||
! TextUtils.equals(newHead.getPriority(), head.getPriority())) {
modifiedNotesCount++;
ContentValues values = new ContentValues();
values.put(ProviderContract.Notes.UpdateParam.TITLE, newHead.getTitle());
values.put(ProviderContract.Notes.UpdateParam.STATE, newHead.getState());
values.put(ProviderContract.Notes.UpdateParam.PRIORITY, newHead.getPriority());
ops.add(ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(ProviderContract.Notes.ContentUri.notes(), cursor.getLong(0)))
.withValues(values)
.build()
);
}
if (listener != null) {
listener.noteParsed(current, total, "Updating notes...");
}
}
} finally {
cursor.close();
}
if (listener != null) {
listener.noteParsed(0, 0, "Updating database...");
}
/*
* Apply batch.
*/
try {
mContext.getContentResolver().applyBatch(ProviderContract.AUTHORITY, ops);
} catch (RemoteException | OperationApplicationException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
notifyDataChanged(mContext);
return modifiedNotesCount;
}
// TODO: Used by tests only for now
public Map<String, BookNamesake> sync() {
try {
Map<String, BookNamesake> nameGroups = groupAllNotebooksByName();
for (BookNamesake group : nameGroups.values()) {
BookAction action = syncNamesake(group);
setBookStatus(group.getBook(), group.getStatus().toString(), action);
}
return nameGroups;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}