package com.orgzly.android.ui.fragments;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.SimpleCursorAdapter;
import android.support.v7.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ViewFlipper;
import com.orgzly.BuildConfig;
import com.orgzly.R;
import com.orgzly.android.SearchQuery;
import com.orgzly.android.Shelf;
import com.orgzly.android.prefs.AppPreferences;
import com.orgzly.android.provider.clients.NotesClient;
import com.orgzly.android.ui.ActionModeListener;
import com.orgzly.android.ui.HeadsListViewAdapter;
import com.orgzly.android.ui.Loaders;
import com.orgzly.android.ui.NoteStateSpinner;
import com.orgzly.android.ui.Selection;
import com.orgzly.android.ui.dialogs.TimestampDialogFragment;
import com.orgzly.android.ui.views.GesturedListView;
import com.orgzly.android.util.LogUtils;
import com.orgzly.org.datetime.OrgDateTime;
import java.util.Set;
import java.util.TreeSet;
/**
* Displays search results.
*/
public class QueryFragment extends NoteListFragment
implements
// TODO: Do we really want OnDateTimeSetListener in fragments, maybe go through activity?
TimestampDialogFragment.OnDateTimeSetListener,
LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = QueryFragment.class.getName();
/** Name used for {@link android.app.FragmentManager}. */
public static final String FRAGMENT_TAG = QueryFragment.class.getName();
/* Arguments. */
private static final String ARG_QUERY = "query";
private static final String ARG_FRESH_INSTANCE = "fresh";
private static final int STATE_ITEM_GROUP = 1;
private SimpleCursorAdapter mListAdapter;
/* Currently active query. */
private SearchQuery mQuery;
private NoteListFragmentListener mListener;
private String mActionModeTag;
private ViewFlipper mViewFlipper;
public static QueryFragment getInstance(String query) {
QueryFragment fragment = new QueryFragment();
Bundle args = new Bundle();
args.putString(ARG_QUERY, query);
args.putBoolean(ARG_FRESH_INSTANCE, true);
fragment.setArguments(args);
return fragment;
}
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public QueryFragment() {
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 = (NoteListFragmentListener) getActivity();
} catch (ClassCastException e) {
throw new ClassCastException(getActivity().toString() + " must implement " + NoteListFragmentListener.class);
}
try {
mActionModeListener = (ActionModeListener) getActivity();
} catch (ClassCastException e) {
throw new ClassCastException(getActivity().toString() + " must implement " + ActionModeListener.class);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, savedInstanceState);
super.onCreate(savedInstanceState);
parseArguments();
if (savedInstanceState != null && savedInstanceState.getBoolean("actionModeMove", false)) {
mActionModeTag = "M";
}
}
@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_query, container, false);
mViewFlipper = (ViewFlipper) view.findViewById(R.id.fragment_query_view_flipper);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, view, savedInstanceState);
super.onViewCreated(view, savedInstanceState);
/* On long click */
getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
mListener.onNoteLongClick(QueryFragment.this, view, position, id);
return true;
}
});
getListView().setOnItemMenuButtonClickListener(
new GesturedListView.OnItemMenuButtonClickListener() {
@Override
public boolean onMenuButtonClick(int buttonId, long noteId) {
switch (buttonId) {
case R.id.item_menu_schedule_btn:
displayScheduleTimestampDialog(R.id.item_menu_schedule_btn, noteId);
break;
case R.id.item_menu_prev_state_btn:
mListener.onStateCycleRequest(noteId, -1);
break;
case R.id.item_menu_next_state_btn:
mListener.onStateCycleRequest(noteId, 1);
break;
case R.id.item_menu_done_state_btn:
if (AppPreferences.isDoneKeyword(getActivity(), "DONE")) {
Set<Long> set = new TreeSet<>();
set.add(noteId);
mListener.onStateChangeRequest(set, "DONE");
}
break;
case R.id.item_menu_open_btn:
mListener.onNoteScrollToRequest(noteId);
break;
}
return false;
}
});
/* Create a selection. */
mSelection = new Selection();
mListAdapter = new HeadsListViewAdapter(getActivity(), mSelection, getListView().getItemMenus(), false);
setListAdapter(mListAdapter);
/**
* Restore selected items, now that adapter is set.
* Saved with {@link Selection#saveSelectedIds(android.os.Bundle, String)}.
*/
mSelection.restoreIds(savedInstanceState);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, savedInstanceState);
super.onActivityCreated(savedInstanceState);
/* Activity created - context available. Create Shelf and populate list with data. */
mShelf = new Shelf(getActivity().getApplicationContext());
mActionModeListener.updateActionModeForSelection(mSelection, new MyActionMode());
/* If query did not change - reuse loader. Otherwise - restart it. */
String newQuery = mQuery.toString();
int id = Loaders.generateLoaderId(Loaders.QUERY_FRAGMENT, newQuery);
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Loader #" + id + " for: " + newQuery);
getActivity().getSupportLoaderManager().initLoader(id, 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();
announceChangesToActivity();
}
private void announceChangesToActivity() {
if (mListener != null) {
mListener.announceChanges(
QueryFragment.FRAGMENT_TAG,
getString(R.string.fragment_query_title),
mQuery.toString(),
mSelection.getCount());
}
}
@Override
public void onDestroyView() {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG);
super.onDestroyView();
setListAdapter(null);
mListAdapter = null;
}
@Override
public void onDetach() {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG);
super.onDetach();
mListener = null;
mActionModeListener = null;
}
private void parseArguments() {
if (getArguments() == null) {
throw new IllegalArgumentException("No arguments found to " + QueryFragment.class.getSimpleName());
}
mQuery = new SearchQuery(getArguments().getString(ARG_QUERY));
}
@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
mListener.onNoteClick(this, view, position, id);
}
/**
* Request small animation for the note in the list.
*/
// public void animateNotes(Set<Long> noteIds, int type) {
// getListAdapter().animateNotes(noteIds, type);
//
// /*
// * After scheduling ids to be animated, must force bindView() to be called.
// * Can't rely on scheduling happening before data is being updated and content provider
// * notifying about the change.
// */
// getListAdapter().notifyDataSetChanged();
// }
@Override
public void onDateTimeSet(int id, TreeSet<Long> noteIds, OrgDateTime time) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, id, time);
switch (id) {
case R.id.query_cab_schedule:
case R.id.item_menu_schedule_btn:
mListener.onScheduledTimeUpdateRequest(noteIds, time);
break;
}
}
@Override
public void onDateTimeCleared(int id, TreeSet<Long> noteIds) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, id);
switch (id) {
case R.id.query_cab_schedule:
case R.id.item_menu_schedule_btn:
mListener.onScheduledTimeUpdateRequest(noteIds, null);
break;
}
}
@Override
public void onDateTimeAborted(int id, TreeSet<Long> noteIds) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, id);
}
@Override
public String getFragmentTag() {
return FRAGMENT_TAG;
}
@Override
public ActionMode.Callback getNewActionMode() {
return new MyActionMode();
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, id, bundle);
return NotesClient.getLoaderForQuery(getActivity(), mQuery);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, loader, cursor);
if (mListAdapter == null) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "adapter is null, view is destroyed?");
return;
}
/**
* 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);
mActionModeListener.updateActionModeForSelection(mSelection, new MyActionMode());
ActionMode actionMode = mActionModeListener.getActionMode();
if (mActionModeTag != null) {
actionMode.setTag("M");
actionMode.invalidate();
mActionModeTag = null;
}
if (mListAdapter.getCount() > 0) {
mViewFlipper.setDisplayedChild(0);
} else {
mViewFlipper.setDisplayedChild(1);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
/* Make sure this is visible fragment with adapter set (onDestroyVieW() not called). */
if (mListAdapter == null) {
return;
}
mListAdapter.changeCursor(null);
}
public class MyActionMode implements ActionMode.Callback {
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, actionMode, menu);
/* Inflate a menu resource providing context menu items. */
MenuInflater inflater = actionMode.getMenuInflater();
inflater.inflate(R.menu.query_cab, menu);
return true;
}
/**
* Called each time the action mode is shown. Always called after onCreateActionMode,
* but may be called multiple times if the mode is invalidated.
*/
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, actionMode, menu);
/* Update action mode with number of selected items. */
actionMode.setTitle(String.valueOf(mSelection.getCount()));
return true;
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, actionMode, menuItem);
switch (menuItem.getItemId()) {
case R.id.query_cab_schedule:
displayScheduleTimestampDialog(R.id.query_cab_schedule, mSelection.getIds());
break;
case R.id.query_cab_state:
/* Add all known states to menu. */
SubMenu subMenu = menuItem.getSubMenu();
if (subMenu != null) {
subMenu.clear();
for (String str: new NoteStateSpinner(getActivity(), null).getValues()) {
subMenu.add(STATE_ITEM_GROUP, Menu.NONE, Menu.NONE, str);
}
}
break;
default:
/* Click on one of the state keywords. */
if (menuItem.getGroupId() == STATE_ITEM_GROUP) {
if (mListener != null) {
mListener.onStateChangeRequest(mSelection.getIds(), menuItem.getTitle().toString());
}
return true;
}
return false; // Not handled.
}
return true; // Handled.
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
mSelection.clearSelection();
/* List adapter could be null, as we could be destroying the action mode of a fragment
* which is in back stack. That fragment had its onDestroyView called, where list
* adapter is set to null.
*/
if (getListAdapter() != null) {
getListAdapter().notifyDataSetChanged();
}
mActionModeListener.actionModeDestroyed();
}
}
public SearchQuery getQuery() {
return mQuery;
}
}