package com.orgzly.android.provider.clients;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import com.orgzly.R;
import com.orgzly.android.Book;
import com.orgzly.android.BookAction;
import com.orgzly.android.BookName;
import com.orgzly.android.prefs.AppPreferences;
import com.orgzly.android.provider.ProviderContract;
import com.orgzly.android.provider.actions.SparseTreeAction;
import com.orgzly.android.repos.Rook;
import com.orgzly.android.repos.VersionedRook;
import com.orgzly.android.util.ExceptionUtils;
import com.orgzly.android.widgets.ListWidgetProvider;
import com.orgzly.org.OrgFileSettings;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class BooksClient {
/** Sort by modification times. */
private static final String ORDER_BY_MTIME =
ProviderContract.Books.Param.IS_DUMMY + "," +
"MAX(COALESCE("+ ProviderContract.Books.Param.MTIME+", 0), COALESCE("+ ProviderContract.Books.Param.SYNCED_ROOK_MTIME+", 0)) DESC, " +
ProviderContract.Books.Param.NAME;
/** Sort by title or filename. */
private static final String ORDER_BY_NAME =
ProviderContract.Books.Param.IS_DUMMY + "," +
"LOWER(COALESCE(" + ProviderContract.Books.Param.TITLE + ", " + ProviderContract.Books.Param.NAME + "))";
private static void toContentValues(ContentValues values, Book book) {
values.put(ProviderContract.Books.Param.NAME, book.getName());
values.put(ProviderContract.Books.Param.PREFACE, book.getPreface());
values.put(ProviderContract.Books.Param.MTIME, book.getMtime());
values.put(ProviderContract.Books.Param.IS_DUMMY, book.isDummy() ? 1 : 0);
if (book.getSyncStatus() != null) {
values.put(ProviderContract.Books.Param.SYNC_STATUS, book.getSyncStatus().toString());
} else {
values.putNull(ProviderContract.Books.Param.SYNC_STATUS);
}
toContentValues(values, book.getOrgFileSettings());
}
public static void toContentValues(ContentValues values, OrgFileSettings settings) {
/* Set title. */
if (settings.getTitle() != null) {
values.put(ProviderContract.Books.Param.TITLE, settings.getTitle());
} else {
values.putNull(ProviderContract.Books.Param.TITLE);
}
values.put(ProviderContract.Books.Param.IS_INDENTED, settings.isIndented() ? 1 : 0);
}
public static Book fromCursor(Cursor cursor) {
Book book = new Book(
cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.NAME)),
cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.PREFACE)),
cursor.getLong(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.MTIME)),
cursor.getInt(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.IS_DUMMY)) == 1
);
book.getOrgFileSettings().setTitle(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.TITLE)));
book.getOrgFileSettings().setIndented(cursor.getInt(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.IS_INDENTED)) == 1);
book.setId(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param._ID)));
book.setSyncStatus(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.SYNC_STATUS)));
book.setDetectedEncoding(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.DETECTED_ENCODING)));
book.setSelectedEncoding(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.SELECTED_ENCODING)));
book.setUsedEncoding(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.USED_ENCODING)));
/* Set link. */
if (! cursor.isNull(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.LINK_ROOK_URL))) {
Uri linkRepoUri = Uri.parse(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.LINK_REPO_URL)));
Uri linkRookUri = Uri.parse(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.LINK_ROOK_URL)));
book.setLink(new Rook(linkRepoUri, linkRookUri));
}
/* Set versioned rook. */
if (! cursor.isNull(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.SYNCED_ROOK_URL))) {
Uri syncRookUri = Uri.parse(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.SYNCED_ROOK_URL)));
Uri syncRepoUri = Uri.parse(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.SYNCED_REPO_URL)));
String rev = cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.SYNCED_ROOK_REVISION));
long mtime = cursor.getLong(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.SYNCED_ROOK_MTIME));
VersionedRook vrook = new VersionedRook(syncRepoUri, syncRookUri, rev, mtime);
book.setLastSyncedToRook(vrook);
}
/* Set last action. */
String lastActionMessage = cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.LAST_ACTION));
if (! TextUtils.isEmpty(lastActionMessage)) {
BookAction.Type lastActionType = BookAction.Type.valueOf(cursor.getString(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.LAST_ACTION_TYPE)));
long lastActionTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(ProviderContract.Books.Param.LAST_ACTION_TIMESTAMP));
BookAction lastAction = new BookAction(lastActionType, lastActionMessage, lastActionTimestamp);
book.setLastAction(lastAction);
}
return book;
}
/**
* @throws IOException if notebook with the same name already exists or some other failure.
*/
public static Book insert(Context context, Book book) throws IOException {
if (doesExist(context, book.getName())) {
throw new IOException("Can't insert notebook with the same name: " + book.getName());
}
ContentValues values = new ContentValues();
BooksClient.toContentValues(values, book);
Uri uri;
try {
uri = context.getContentResolver().insert(ProviderContract.Books.ContentUri.books(), values);
} catch (Exception e) {
throw ExceptionUtils.IOException(e, "Failed inserting book " + book.getName());
}
book.setId(ContentUris.parseId(uri));
return book;
}
/**
* Update book's modified time.
*/
public static int setModifiedTime(Context context, long bookId, long time) {
ContentValues values = new ContentValues();
values.put(ProviderContract.Books.Param.MTIME, time);
return context.getContentResolver().update(ContentUris.withAppendedId(ProviderContract.Books.ContentUri.books(), bookId), values, null, null);
}
/**
* Update book's link URL.
*/
public static int setLink(Context context, long bookId, String repoUrl, String rookUrl) {
ContentValues values = new ContentValues();
values.put(ProviderContract.BookLinks.Param.REPO_URL, repoUrl);
values.put(ProviderContract.BookLinks.Param.ROOK_URL, rookUrl);
return context.getContentResolver().update(ProviderContract.BookLinks.ContentUri.booksIdLinks(bookId), values, null, null);
}
public static int removeLink(Context context, long bookId) {
return context.getContentResolver().delete(ProviderContract.BookLinks.ContentUri.booksIdLinks(bookId), null, null);
}
/**
* Deletes a single book.
*
* @param id row ID of the book to delete
*/
public static void delete(Context context, long id) {
context.getContentResolver().delete(ContentUris.withAppendedId(ProviderContract.Books.ContentUri.books(), id), null, null);
}
public static List<Book> getAll(Context context) {
List<Book> books = new ArrayList<>();
Cursor cursor = context.getContentResolver().query(
ProviderContract.Books.ContentUri.books(), null, null, null, getSortOrder(context));
try {
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
books.add(fromCursor(cursor));
}
} finally {
cursor.close();
}
return books;
}
/**
* Get book from database by its ID.
*
* @param bookId ID for the {@link Book}
* @return {@link Book} or {@code null} if the book with specified ID doesn't exist
*/
public static Book get(Context context, long bookId) {
Cursor cursor = context.getContentResolver().query(
ProviderContract.Books.ContentUri.booksId(bookId), null, null, null, null);
try {
if (cursor.moveToFirst()) {
return fromCursor(cursor);
} else {
return null;
}
} finally {
cursor.close();
}
}
/**
* Get book from database by its name.
*
* @param name Name of the book
* @return {@link Book} or {@code null} if the book with specified name doesn't exist
*/
public static Book get(Context context, String name) {
Cursor cursor = context.getContentResolver().query(
ProviderContract.Books.ContentUri.books(),
null,
ProviderContract.Books.Param.NAME + "=?",
new String[] { name },
null);
try {
if (cursor.moveToFirst()) {
return fromCursor(cursor);
} else {
return null;
}
} finally {
cursor.close();
}
}
/** Checks if notebook with the same name already exists in database. */
public static boolean doesExist(Context context, String name) {
Cursor cursor = context.getContentResolver().query(ProviderContract.Books.ContentUri.books(), null, ProviderContract.Books.Param.NAME + " = ?", new String[] { name }, null);
try {
return cursor.getCount() > 0;
} finally {
cursor.close();
}
}
/**
* Stores synchronization message to database, prepended with current time.
*/
public static int updateStatus(Context context, long bookId, String status, BookAction action) {
ContentValues values = new ContentValues();
// TODO: Do we even need status in DB? Is it used except for tests?
if (status != null) {
values.put(ProviderContract.Books.Param.SYNC_STATUS, status);
} else {
values.putNull(ProviderContract.Books.Param.SYNC_STATUS);
}
values.put(ProviderContract.Books.Param.LAST_ACTION, action.getMessage());
values.put(ProviderContract.Books.Param.LAST_ACTION_TIMESTAMP, action.getTimestamp());
values.put(ProviderContract.Books.Param.LAST_ACTION_TYPE, action.getType().toString());
return context.getContentResolver().update(ContentUris.withAppendedId(ProviderContract.Books.ContentUri.books(), bookId), values, null, null);
}
public static int updateSettings(Context context, Book book) {
ContentValues values = new ContentValues();
values.put(ProviderContract.Books.Param.PREFACE, book.getPreface());
values.put(ProviderContract.Books.Param.TITLE, book.getOrgFileSettings().getTitle());
values.put(ProviderContract.Books.Param.MTIME, System.currentTimeMillis());
return context.getContentResolver().update(
ContentUris.withAppendedId(ProviderContract.Books.ContentUri.books(), book.getId()), values, null, null);
}
public static int updateName(Context context, long id, String name) {
ContentValues values = new ContentValues();
values.put(ProviderContract.Books.Param.NAME, name);
return context.getContentResolver().update(ContentUris.withAppendedId(ProviderContract.Books.ContentUri.books(), id), values, null, null);
}
public static int promote(Context context, long bookId, Set<Long> noteIds) {
ContentValues values = new ContentValues();
values.put(ProviderContract.Promote.Param.BOOK_ID, bookId);
values.put(ProviderContract.Promote.Param.IDS, TextUtils.join(",", noteIds));
return context.getContentResolver().update(ProviderContract.Promote.ContentUri.promote(), values, null, null);
}
public static int demote(Context context, long bookId, Set<Long> noteIds) {
ContentValues values = new ContentValues();
values.put(ProviderContract.Demote.Param.BOOK_ID, bookId);
values.put(ProviderContract.Demote.Param.IDS, TextUtils.join(",", noteIds));
return context.getContentResolver().update(ProviderContract.Demote.ContentUri.demote(), values, null, null);
}
public static void cycleVisibility(Context context, Book book) {
context.getContentResolver().update(ProviderContract.Books.ContentUri.booksIdCycleVisibility(book.getId()), null, null, null);
}
public static int moveNotes(Context context, long bookId, Long noteId, int direction) {
ContentValues values = new ContentValues();
values.put(ProviderContract.Move.Param.BOOK_ID, bookId);
values.put(ProviderContract.Move.Param.IDS, noteId);
values.put(ProviderContract.Move.Param.DIRECTION, direction);
return context.getContentResolver().update(ProviderContract.Move.ContentUri.move(), values, null, null);
}
public static Uri loadFromFile(Context context, String name, BookName.Format format, File file, VersionedRook vrook, String selectedEncoding) throws IOException {
ContentValues values = new ContentValues();
values.put(ProviderContract.LoadBookFromFile.Param.BOOK_NAME, name);
values.put(ProviderContract.LoadBookFromFile.Param.FORMAT, format.toString());
values.put(ProviderContract.LoadBookFromFile.Param.FILE_PATH, file.getAbsolutePath());
if (vrook != null) {
values.put(ProviderContract.LoadBookFromFile.Param.ROOK_REPO_URL, vrook.getRepoUri().toString());
values.put(ProviderContract.LoadBookFromFile.Param.ROOK_URL, vrook.getUri().toString());
values.put(ProviderContract.LoadBookFromFile.Param.ROOK_REVISION, vrook.getRevision());
values.put(ProviderContract.LoadBookFromFile.Param.ROOK_MTIME, vrook.getMtime());
}
if (selectedEncoding != null) {
values.put(ProviderContract.LoadBookFromFile.Param.SELECTED_ENCODING, selectedEncoding);
}
try {
return context.getContentResolver().insert(ProviderContract.LoadBookFromFile.ContentUri.loadBookFromFile(), values);
} catch (IllegalArgumentException e) {
throw ExceptionUtils.IOException(e, "Failed loading book " + name);
// FIXME: We sometimes catch these exceptions from content provider, sometimes not
}
}
public static void saved(Context context, long id, VersionedRook uploadedBook) {
ContentValues values = new ContentValues();
values.put(ProviderContract.BooksIdSaved.Param.REPO_URL, uploadedBook.getRepoUri().toString());
values.put(ProviderContract.BooksIdSaved.Param.ROOK_URL, uploadedBook.getUri().toString());
values.put(ProviderContract.BooksIdSaved.Param.ROOK_REVISION, uploadedBook.getRevision());
values.put(ProviderContract.BooksIdSaved.Param.ROOK_MTIME, uploadedBook.getMtime());
context.getContentResolver().insert(ProviderContract.BooksIdSaved.ContentUri.booksIdSaved(id), values);
}
public static Loader<Cursor> getCursorLoader(Context context) {
return new CursorLoader(
context,
ProviderContract.Books.ContentUri.books(),
null,
null,
null,
getSortOrder(context));
}
private static String getSortOrder(Context context) {
String sortOrder = ORDER_BY_NAME;
if (context.getString(R.string.pref_value_notebooks_sort_order_modification_time).equals(AppPreferences.notebooksSortOrder(context))) {
sortOrder = ORDER_BY_MTIME;
}
return sortOrder;
}
public static void sparseTree(Context context, long bookId, long noteId) {
ContentValues values = new ContentValues();
values.put(SparseTreeAction.ID, noteId);
context.getContentResolver().update(ProviderContract.Books.ContentUri.booksIdSparseTree(bookId), values, null, null);
}
}