package com.orgzly.android.ui.fragments;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Typeface;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.SimpleCursorAdapter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import com.orgzly.BuildConfig;
import com.orgzly.R;
import com.orgzly.android.Book;
import com.orgzly.android.BookAction;
import com.orgzly.android.prefs.AppPreferences;
import com.orgzly.android.provider.ProviderContract;
import com.orgzly.android.provider.clients.BooksClient;
import com.orgzly.android.ui.Fab;
import com.orgzly.android.ui.FragmentListener;
import com.orgzly.android.ui.Loaders;
import com.orgzly.android.util.LogUtils;
import com.orgzly.android.util.UriUtils;
import java.text.DateFormat;
import java.util.Date;
/**
* Displays all notebooks.
* Allows creating new, deleting, renaming, setting links etc.
*/
public class BooksFragment extends ListFragment
implements
Fab,
LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = BooksFragment.class.getName();
/** Name used for {@link android.app.FragmentManager}. */
public static final String FRAGMENT_TAG = BooksFragment.class.getName();
private static final String ARG_ADD_OPTIONS = "add_options";
private static final String ARG_SHOW_CONTEXT_MENU = "show_context_menu";
private BooksFragmentListener mListener;
private SimpleCursorAdapter mListAdapter;
private View mNoNotebookText;
private boolean mIsViewCreated = false;
private boolean mAddOptions = true;
private boolean mShowContextMenu = true;
public static BooksFragment getInstance() {
return getInstance(true, true);
}
public static BooksFragment getInstance(boolean addOptions, boolean showContextMenu) {
BooksFragment fragment = new BooksFragment();
Bundle args = new Bundle();
args.putBoolean(ARG_ADD_OPTIONS, addOptions);
args.putBoolean(ARG_SHOW_CONTEXT_MENU, showContextMenu);
fragment.setArguments(args);
return fragment;
}
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public BooksFragment() {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG);
}
@Override
public void onAttach(Context context) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, getActivity());
super.onAttach(context);
/* This makes sure that the container activity has implemented
* the callback interface. If not, it throws an exception
*/
try {
mListener = (BooksFragmentListener) getActivity();
} catch (ClassCastException e) {
throw new ClassCastException(getActivity().toString() + " must implement " + BooksFragmentListener.class);
}
parseArguments();
}
private void parseArguments() {
if (getArguments() == null) {
throw new IllegalArgumentException("No arguments found to " + BooksFragment.class.getSimpleName());
}
mAddOptions = getArguments().getBoolean(ARG_ADD_OPTIONS);
mShowContextMenu = getArguments().getBoolean(ARG_SHOW_CONTEXT_MENU);
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, savedInstanceState);
super.onCreate(savedInstanceState);
/* Would like to add items to the Options Menu.
* Required (for fragments only) to receive onCreateOptionsMenu() call.
*/
setHasOptionsMenu(mAddOptions);
mListAdapter = createAdapter();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_books, container, false);
mNoNotebookText = view.findViewById(R.id.fragment_books_no_notebooks);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, view, savedInstanceState);
super.onViewCreated(view, savedInstanceState);
mIsViewCreated = true;
/* Request callbacks for Context menu. */
registerForContextMenu(getListView());
setListAdapter(mListAdapter);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, savedInstanceState);
super.onActivityCreated(savedInstanceState);
getActivity().getSupportLoaderManager().initLoader(Loaders.BOOKS_FRAGMENT, null, this);
}
@Override
public void onViewStateRestored(Bundle savedInstanceState) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, savedInstanceState);
super.onViewStateRestored(savedInstanceState);
}
@Override
public void onResume() {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG);
super.onResume();
/* In case books sort order preference has been changed. */
getActivity().getSupportLoaderManager().restartLoader(Loaders.BOOKS_FRAGMENT, null, this);
announceChangesToActivity();
}
@Override
public void onDestroyView() {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG);
super.onDestroyView();
mIsViewCreated = false;
}
@Override
public void onDetach() {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG);
super.onDetach();
mListener = null;
}
private SimpleCursorAdapter createAdapter() {
SimpleCursorAdapter adapter;
/* Column field names to be bound. */
String[] columns = new String[] {
ProviderContract.Books.Param.NAME,
ProviderContract.Books.Param.NAME,
ProviderContract.Books.Param.MTIME,
ProviderContract.Books.Param.LAST_ACTION,
ProviderContract.Books.Param.LINK_REPO_URL,
ProviderContract.Books.Param.LINK_ROOK_URL,
ProviderContract.Books.Param.SYNCED_REPO_URL,
ProviderContract.Books.Param.SYNCED_ROOK_URL,
ProviderContract.Books.Param.SYNCED_ROOK_REVISION,
ProviderContract.Books.Param.SYNCED_ROOK_MTIME,
ProviderContract.Books.Param.USED_ENCODING,
ProviderContract.Books.Param.DETECTED_ENCODING,
ProviderContract.Books.Param.SELECTED_ENCODING,
};
/* Views which the data will be bound to. */
int[] to = new int[] {
R.id.item_book_title,
R.id.item_book_subtitle,
R.id.item_book_mtime,
R.id.item_book_last_action,
R.id.item_book_link_repo,
R.id.item_book_link_url,
R.id.item_book_synced_repo,
R.id.item_book_synced_url,
R.id.item_book_synced_revision,
R.id.item_book_synced_mtime,
R.id.item_book_encoding_used,
R.id.item_book_encoding_detected,
R.id.item_book_encoding_selected
};
adapter = new SimpleCursorAdapter(
getActivity(),
R.layout.item_book,
null,
columns,
to,
0) {
class ViewHolder {
TextView title;
View modifiedAfterSyncIcon;
View bookDetailsPadding;
View mtimeContainer;
View linkDetailsContainer;
View versionedRookContainer;
View versionedRookUrlContainer;
View versionedRookMtimeContainer;
View versionedRookRevisionContainer;
View lastActionContainer;
View usedEncodingContainer;
View detectedEncodingContainer;
View selectedEncodingContainer;
TextView lastAction;
TextView subTitle;
TextView mtime;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
super.bindView(view, context, cursor);
boolean isBookDetailDisplayed = false;
ViewHolder holder = (ViewHolder) view.getTag();
if (holder == null) {
holder = new ViewHolder();
holder.title = (TextView) view.findViewById(R.id.item_book_title);
holder.modifiedAfterSyncIcon = view.findViewById(R.id.item_book_modified_after_sync_icon);
holder.bookDetailsPadding = view.findViewById(R.id.item_book_details_padding);
holder.subTitle = (TextView) view.findViewById(R.id.item_book_subtitle);
holder.mtimeContainer = view.findViewById(R.id.item_book_mtime_container);
holder.mtime = (TextView) view.findViewById(R.id.item_book_mtime);
holder.linkDetailsContainer = view.findViewById(R.id.item_book_link_container);
holder.versionedRookContainer = view.findViewById(R.id.item_book_synced_container);
holder.versionedRookUrlContainer = view.findViewById(R.id.item_book_synced_url_container);
holder.versionedRookMtimeContainer = view.findViewById(R.id.item_book_synced_mtime_container);
holder.versionedRookRevisionContainer = view.findViewById(R.id.item_book_synced_revision_container);
holder.lastActionContainer = view.findViewById(R.id.item_book_last_action_container);
holder.lastAction = (TextView) view.findViewById(R.id.item_book_last_action);
holder.usedEncodingContainer = view.findViewById(R.id.item_book_encoding_used_container);
holder.detectedEncodingContainer = view.findViewById(R.id.item_book_encoding_detected_container);
holder.selectedEncodingContainer = view.findViewById(R.id.item_book_encoding_selected_container);
view.setTag(holder);
}
Book book = BooksClient.fromCursor(cursor);
/*
* If title exists - use title and set book's name as a subtitle.
* If title does no exist - use book's name hide the subtitle.
*/
if (book.getOrgFileSettings().getTitle() != null) {
holder.title.setText(book.getOrgFileSettings().getTitle());
holder.subTitle.setText(book.getName());
holder.subTitle.setVisibility(View.VISIBLE);
} else {
holder.title.setText(book.getName());
holder.subTitle.setVisibility(View.GONE);
}
if (book.isModifiedAfterLastSync()) {
holder.modifiedAfterSyncIcon.setVisibility(View.VISIBLE);
} else {
holder.modifiedAfterSyncIcon.setVisibility(View.INVISIBLE);
}
/*
* Modification time.
*/
if (AppPreferences.displayedBookDetails(context).contains(getString(R.string.pref_value_book_details_mtime))) {
holder.mtimeContainer.setVisibility(View.VISIBLE);
isBookDetailDisplayed = true;
} else {
holder.mtimeContainer.setVisibility(View.GONE);
}
/* If book has no link - remove related rows. */
if (book.getLink() != null && AppPreferences.displayedBookDetails(context).contains(getString(R.string.pref_value_book_details_link_url))) {
holder.linkDetailsContainer.setVisibility(View.VISIBLE);
isBookDetailDisplayed = true;
} else {
holder.linkDetailsContainer.setVisibility(View.GONE);
}
/* If book has no versioned rook - remove all related rows. */
if (book.getLastSyncedToRook() != null) {
holder.versionedRookContainer.setVisibility(View.VISIBLE);
if (AppPreferences.displayedBookDetails(context).contains(getString(R.string.pref_value_book_details_sync_url))) {
holder.versionedRookUrlContainer.setVisibility(View.VISIBLE);
isBookDetailDisplayed = true;
} else {
holder.versionedRookUrlContainer.setVisibility(View.GONE);
}
if (AppPreferences.displayedBookDetails(context).contains(getString(R.string.pref_value_book_details_sync_mtime))) {
holder.versionedRookMtimeContainer.setVisibility(View.VISIBLE);
isBookDetailDisplayed = true;
} else {
holder.versionedRookMtimeContainer.setVisibility(View.GONE);
}
if (AppPreferences.displayedBookDetails(context).contains(getString(R.string.pref_value_book_details_sync_revision))) {
holder.versionedRookRevisionContainer.setVisibility(View.VISIBLE);
isBookDetailDisplayed = true;
} else {
holder.versionedRookRevisionContainer.setVisibility(View.GONE);
}
} else {
holder.versionedRookContainer.setVisibility(View.GONE);
}
/* Hide last action if
* - there is no last action (sync) performed
* OR
* - action is INFO but user choose not to display it
*/
if (book.getLastAction() == null || (book.getLastAction().getType() == BookAction.Type.INFO && !AppPreferences.displayedBookDetails(context).contains(getString(R.string.pref_value_book_details_last_action)))) {
holder.lastActionContainer.setVisibility(View.GONE);
} else {
holder.lastActionContainer.setVisibility(View.VISIBLE);
isBookDetailDisplayed = true;
SpannableStringBuilder builder = new SpannableStringBuilder();
builder.append(DateFormat.getDateTimeInstance().format(new Date(book.getLastAction().getTimestamp())));
builder.append(": ");
int pos = builder.length();
builder.append(book.getLastAction().getMessage());
if (book.getLastAction().getType() == BookAction.Type.ERROR) {
/* Get error color attribute. */
TypedArray arr = getActivity().obtainStyledAttributes(
new int[] { R.attr.item_book_error_color });
int color = arr.getColor(0, 0);
arr.recycle();
/* Set error color. */
builder.setSpan(new ForegroundColorSpan(color), pos, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (book.getLastAction().getType() == BookAction.Type.PROGRESS) {
builder.setSpan(new StyleSpan(Typeface.BOLD), pos, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
holder.lastAction.setText(builder);
}
/* If encoding is not set, removed it. */
if (book.getUsedEncoding() != null && AppPreferences.displayedBookDetails(context).contains(getString(R.string.pref_value_book_details_encoding_used))) {
holder.usedEncodingContainer.setVisibility(View.VISIBLE);
isBookDetailDisplayed = true;
} else {
holder.usedEncodingContainer.setVisibility(View.GONE);
}
if (book.getDetectedEncoding() != null && AppPreferences.displayedBookDetails(context).contains(getString(R.string.pref_value_book_details_encoding_detected))) {
holder.detectedEncodingContainer.setVisibility(View.VISIBLE);
isBookDetailDisplayed = true;
} else {
holder.detectedEncodingContainer.setVisibility(View.GONE);
}
if (book.getSelectedEncoding() != null) {
holder.selectedEncodingContainer.setVisibility(View.VISIBLE);
} else {
holder.selectedEncodingContainer.setVisibility(View.GONE);
}
/* If it's a dummy book - change opacity. */
if (book.isDummy()) {
view.setAlpha(0.4f);
} else {
view.setAlpha(1);
}
/*
* Add some vertical spacing if at least one of the notebook details is displayed.
*/
if (isBookDetailDisplayed) {
holder.bookDetailsPadding.setVisibility(View.VISIBLE);
} else {
holder.bookDetailsPadding.setVisibility(View.GONE);
}
}
};
adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
TextView textView;
switch (view.getId()) {
case R.id.item_book_encoding_used:
if (! cursor.isNull(columnIndex)) {
textView = (TextView) view;
textView.setText(cursor.getString(columnIndex));
textView.append(" used");
}
return true;
case R.id.item_book_encoding_detected:
if (! cursor.isNull(columnIndex)) {
textView = (TextView) view;
textView.setText(cursor.getString(columnIndex));
textView.append(" detected");
}
return true;
case R.id.item_book_encoding_selected:
if (! cursor.isNull(columnIndex)) {
textView = (TextView) view;
textView.setText(cursor.getString(columnIndex));
textView.append(" selected");
}
return true;
/* Generic N/A-if-does-not-exist. */
case R.id.item_book_synced_revision:
textView = (TextView) view;
if (! cursor.isNull(columnIndex)) {
textView.setText(cursor.getString(columnIndex));
} else {
textView.setText("N/A");
}
return true;
case R.id.item_book_synced_mtime:
textView = (TextView) view;
if (! cursor.isNull(columnIndex) && cursor.getLong(columnIndex) > 0) {
/* Format time. */
textView.setText(DateFormat.getDateTimeInstance().format(new Date(cursor.getLong(columnIndex))));
} else {
textView.setText("N/A");
}
return true;
case R.id.item_book_mtime:
textView = (TextView) view;
if (! cursor.isNull(columnIndex) && cursor.getLong(columnIndex) > 0) {
/* Format time. */
textView.setText(DateFormat.getDateTimeInstance().format(new Date(cursor.getLong(columnIndex))));
} else {
textView.setText("Never modified locally");
}
return true;
case R.id.item_book_link_url:
case R.id.item_book_synced_url:
textView = (TextView) view;
if (! cursor.isNull(columnIndex)) {
textView.setText(UriUtils.friendlyUri(cursor.getString(columnIndex)));
}
return true;
}
return false;
}
});
return adapter;
}
/**
* Callback for options menu.
*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, menu, inflater);
inflater.inflate(R.menu.books_actions, menu);
}
/**
* Callback for options menu.
* Called after each invalidateOptionsMenu().
*/
// @Override
// public boolean onPrepareOptionsMenu(Menu menu) {
// return true;
// }
/**
* Callback for options menu.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.books_options_menu_item_import_book:
mListener.onBookLoadRequest();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onListItemClick(ListView listView, View v, int position, long id) {
super.onListItemClick(listView, v, position, id);
if (mListener != null) {
mListener.onBookClicked(id);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
if (mShowContextMenu) {
getActivity().getMenuInflater().inflate(R.menu.books_context, menu);
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
/* Get ID of the item. */
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
long bookId = info.id;
switch (item.getItemId()) {
case R.id.books_context_menu_rename: /* Rename book. */
mListener.onBookRenameRequest(bookId);
return true;
case R.id.books_context_menu_set_link: /* Set link to remote book. */
mListener.onBookLinkSetRequest(bookId);
return true;
case R.id.books_context_menu_force_save:
mListener.onForceSaveRequest(bookId);
return true;
case R.id.books_context_menu_force_load:
mListener.onForceLoadRequest(bookId);
return true;
case R.id.books_context_menu_export: /* Export book. */
mListener.onBookExportRequest(bookId);
return true;
case R.id.books_context_menu_delete: /* Delete book. */
mListener.onBookDeleteRequest(bookId);
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, id, bundle);
return BooksClient.getCursorLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, loader, cursor);
if (mIsViewCreated) {
/**
* Swapping instead of changing Cursor here, to keep the old one open.
* Loader should release the old Cursor - see note in
* {@link LoaderManager.LoaderCallbacks#onLoadFinished).
*/
mListAdapter.swapCursor(cursor);
if (mListAdapter.getCount() > 0) {
mNoNotebookText.setVisibility(View.GONE);
} else {
mNoNotebookText.setVisibility(View.VISIBLE);
}
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (mIsViewCreated) {
mListAdapter.changeCursor(null);
}
}
@Override
public Runnable getFabAction() {
return new Runnable() {
@Override
public void run() {
mListener.onBookCreateRequest();
}
};
}
private void announceChangesToActivity() {
if (mListener != null) {
mListener.announceChanges(
FRAGMENT_TAG,
getString(R.string.notebooks),
null,
0); // No books ever selected, as we're using the old floating context menu.
}
}
public interface BooksFragmentListener extends FragmentListener {
/**
* Request for creating new book.
*/
void onBookCreateRequest();
/**
* Click on a book item has been performed.
* @param bookId
*/
void onBookClicked(long bookId);
/**
* User wants to delete the book.
* @param bookId
*/
void onBookDeleteRequest(long bookId);
void onBookRenameRequest(long bookId);
void onBookLinkSetRequest(long bookId);
void onForceSaveRequest(long bookId);
void onForceLoadRequest(long bookId);
void onBookExportRequest(long bookId);
void onBookLoadRequest();
}
}