package org.commcare.activities;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.text.Spannable;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.SearchView;
import android.widget.TextView;
import org.commcare.CommCareApplication;
import org.commcare.android.database.user.models.ACase;
import org.commcare.fragments.BreadcrumbBarFragment;
import org.commcare.fragments.ContainerFragment;
import org.commcare.fragments.TaskConnectorFragment;
import org.commcare.interfaces.WithUIController;
import org.commcare.logging.AndroidLogger;
import org.commcare.session.SessionFrame;
import org.commcare.session.SessionInstanceBuilder;
import org.commcare.suite.model.Detail;
import org.commcare.suite.model.StackFrameStep;
import org.commcare.tasks.templates.CommCareTask;
import org.commcare.tasks.templates.CommCareTaskConnector;
import org.commcare.utils.AndroidUtil;
import org.commcare.utils.ConnectivityStatus;
import org.commcare.utils.MarkupUtil;
import org.commcare.utils.SessionStateUninitException;
import org.commcare.views.ManagedUiFramework;
import org.commcare.views.dialogs.StandardAlertDialog;
import org.commcare.views.dialogs.AlertDialogFragment;
import org.commcare.views.dialogs.CommCareAlertDialog;
import org.commcare.views.dialogs.CustomProgressDialog;
import org.commcare.views.dialogs.DialogController;
import org.commcare.views.media.AudioController;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.util.NoLocalizedTextException;
/**
* Base class for CommCareActivities to simplify
* common localization and workflow tasks
*
* @author ctsims
*/
public abstract class CommCareActivity<R> extends FragmentActivity
implements CommCareTaskConnector<R>, DialogController, OnGestureListener {
private static final String TAG = CommCareActivity.class.getSimpleName();
private static final String KEY_PROGRESS_DIALOG_FRAG = "progress-dialog-fragment";
private static final String KEY_ALERT_DIALOG_FRAG = "alert-dialog-fragment";
private int invalidTaskIdMessageThrown = -2;
private TaskConnectorFragment<R> stateHolder;
// Fields for implementing task transitions for CommCareTaskConnector
private boolean inTaskTransition;
/**
* Used to indicate that the (progress) dialog associated with a task
* should be dismissed because the task has completed or been canceled.
*/
private boolean dismissLastDialogAfterTransition = true;
private AlertDialogFragment alertDialogToShowOnResume;
private GestureDetector mGestureDetector;
protected String lastQueryString;
/**
* Activity has been put in the background. Flag prevents dialogs
* from being shown while activity isn't active.
*/
private boolean areFragmentsPaused = true;
/**
* Mark when task tried to show progress dialog before fragments have resumed,
* so that the dialog can be shown when fragments have fully resumed.
*/
private boolean triedBlockingWhilePaused;
private boolean triedDismissingWhilePaused;
/**
* Store the id of a task progress dialog so it can be disabled/enabled
* on activity pause/resume.
*/
private int dialogId = -1;
private ContainerFragment<Bundle> managedUiState;
private boolean isMainScreenBlocked;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentManager fm = this.getSupportFragmentManager();
stateHolder = (TaskConnectorFragment<R>) fm.findFragmentByTag("state");
// stateHolder and its previous state aren't null if the activity is
// being created due to an orientation change.
if (stateHolder == null) {
stateHolder = new TaskConnectorFragment<>();
fm.beginTransaction().add(stateHolder, "state").commit();
// entering new activity, not just rotating one, so release old
// media
AudioController.INSTANCE.releaseCurrentMediaEntity();
}
// For activities using a uiController, this must be called before persistManagedUiState()
if (usesUIController()) {
((WithUIController)this).initUIController();
}
persistManagedUiState(fm);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && shouldShowBreadcrumbBar()) {
getActionBar().setDisplayShowCustomEnabled(true);
// Add breadcrumb bar
BreadcrumbBarFragment bar = (BreadcrumbBarFragment) fm.findFragmentByTag("breadcrumbs");
// If the state holder is null, create a new one for this activity
if (bar == null) {
bar = new BreadcrumbBarFragment();
fm.beginTransaction().add(bar, "breadcrumbs").commit();
}
}
mGestureDetector = new GestureDetector(this, this);
}
private void persistManagedUiState(FragmentManager fm) {
if (isManagedUiActivity()) {
managedUiState = (ContainerFragment)fm.findFragmentByTag("ui-state");
if (managedUiState == null) {
managedUiState = new ContainerFragment<>();
fm.beginTransaction().add(managedUiState, "ui-state").commit();
loadUiElementState(null);
} else {
loadUiElementState(managedUiState.getData());
}
}
}
private void loadUiElementState(Bundle savedInstanceState) {
ManagedUiFramework.setContentView(this);
if (savedInstanceState != null) {
ManagedUiFramework.restoreUiElements(this, savedInstanceState);
} else {
ManagedUiFramework.loadUiElements(this);
}
}
/**
* Call this method from an implementing activity to request a new event trigger for any time
* the available space for the core content view changes significantly, for instance when the
* soft keyboard is displayed or hidden.
*
* This method will also be reliably triggered upon the end of the first layout pass, so it
* can be used to do the initial setup for adaptive layouts as well as their updates.
*
* After this is called, major layout size changes will be triggered in the onMajorLayoutChange
* method.
*/
protected void requestMajorLayoutUpdates() {
final View decorView = getWindow().getDecorView();
decorView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
int mPreviousDecorViewFrameHeight = 0;
@Override
public void onGlobalLayout() {
Rect r = new Rect();
//r will be populated with the coordinates of your view that are visible after the
//recent change.
decorView.getWindowVisibleDisplayFrame(r);
int mainContentHeight = r.height();
int previousMeasurementDifference = Math.abs(mainContentHeight - mPreviousDecorViewFrameHeight);
if (previousMeasurementDifference > 100) {
onMajorLayoutChange(r);
}
mPreviousDecorViewFrameHeight = mainContentHeight;
}
});
}
/**
* This method is called when the root view size available to the activity has changed
* significantly. It is the appropriate place to trigger adaptive layout behaviors.
*
* Note for performance that changes to declarative view properties here will trigger another
* layout pass.
*
* This callback is only triggered if the parent view has called requestMajorLayoutUpdates
*
* @param newRootViewDimensions The dimensions of the new root screen view that is available
* to the activity.
*/
protected void onMajorLayoutChange(Rect newRootViewDimensions) {
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
this.onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
protected boolean isTopNavEnabled() {
return false;
}
/**
* If a message for the user has been set in CommCareApplication, show it and then clear it
*/
private void showPendingUserMessage() {
String[] messageAndTitle = CommCareApplication.instance().getPendingUserMessage();
if (messageAndTitle != null) {
showAlertDialog(StandardAlertDialog.getBasicAlertDialog(
this, messageAndTitle[1], messageAndTitle[0], null));
CommCareApplication.instance().clearPendingUserMessage();
}
}
@Override
protected void onResume() {
super.onResume();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
// In honeycomb and above the fragment takes care of this
this.setTitle(getTitle(this, getActivityTitle()));
}
AudioController.INSTANCE.playPreviousAudio();
}
@Override
protected void onResumeFragments() {
super.onResumeFragments();
areFragmentsPaused = false;
syncTaskBlockingWithDialogFragment();
showPendingAlertDialog();
}
@Override
protected void onPause() {
super.onPause();
if (isManagedUiActivity()) {
managedUiState.setData(ManagedUiFramework.saveUiStateToBundle(this));
}
areFragmentsPaused = true;
AudioController.INSTANCE.systemInducedPause();
}
@Override
public <A, B, C> void connectTask(CommCareTask<A, B, C, R> task) {
stateHolder.connectTask(task, this);
// If we've left an old dialog showing during the task transition and it was from the same
// task as the one that is starting, we want to just leave it up for the next task too
CustomProgressDialog currDialog = getCurrentProgressDialog();
if (currDialog != null && currDialog.getTaskId() == task.getTaskId()) {
dismissLastDialogAfterTransition = false;
}
}
/**
* @return wakelock level for an activity with a running task attached to
* it; defaults to not using wakelocks.
*/
public int getWakeLockLevel() {
return CommCareTask.DONT_WAKELOCK;
}
/**
* Sync progress dialog fragment with any task state changes that may have
* occurred while the activity was paused.
*/
private void syncTaskBlockingWithDialogFragment() {
if (triedDismissingWhilePaused) {
triedDismissingWhilePaused = false;
dismissProgressDialog();
} else if (triedBlockingWhilePaused) {
triedBlockingWhilePaused = false;
showNewProgressDialog();
}
}
@Override
public void startBlockingForTask(int id) {
dialogId = id;
if (areFragmentsPaused) {
// post-pone dialog transactions until after fragments have fully resumed.
triedBlockingWhilePaused = true;
} else {
showNewProgressDialog();
}
}
private void showNewProgressDialog() {
// Only show a new dialog if we chose to dismiss the old one; If
// dismissLastDialogAfterTransition is false, that means we left the last dialog up and do
// not need to create a new one
if (dismissLastDialogAfterTransition) {
dismissProgressDialog();
showProgressDialog(dialogId);
}
}
@Override
public void stopBlockingForTask(int id) {
dialogId = -1;
if (id >= 0) {
if (inTaskTransition) {
dismissLastDialogAfterTransition = true;
} else {
dismissProgressDialog();
}
}
stateHolder.releaseWakeLock();
}
@Override
public R getReceiver() {
return (R) this;
}
@Override
public void startTaskTransition() {
inTaskTransition = true;
}
@Override
public void stopTaskTransition() {
inTaskTransition = false;
if (dismissLastDialogAfterTransition) {
dismissProgressDialog();
// Re-set shouldDismissDialog to true after this transition cycle is over
dismissLastDialogAfterTransition = true;
}
}
/**
* Display exception details as a pop-up to the user.
*/
private void displayException(String title, String message) {
DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int i) {
switch (i) {
case DialogInterface.BUTTON_POSITIVE:
finish();
break;
}
}
};
showAlertDialog(StandardAlertDialog.getBasicAlertDialogWithIcon(this, title,
message, android.R.drawable.ic_dialog_info, listener));
}
public void displayCaseListFilterException(Exception e) {
displayException(
Localization.get("notification.case.predicate.title"),
Localization.get("notification.case.predicate.action", new String[]{e.getMessage()}));
}
@Override
public void taskCancelled() {
}
public void cancelCurrentTask() {
stateHolder.cancelTask();
}
protected void restoreLastQueryString() {
lastQueryString = (String)CommCareApplication.instance().getCurrentSession().getCurrentFrameStepExtra(SessionInstanceBuilder.KEY_LAST_QUERY_STRING);
}
protected void saveLastQueryString() {
CommCareApplication.instance().getCurrentSession().addExtraToCurrentFrameStep(SessionInstanceBuilder.KEY_LAST_QUERY_STRING, lastQueryString);
}
//Graphical stuff below, needs to get modularized
protected void transplantStyle(TextView target, int resource) {
//get styles from here
TextView tv = (TextView) View.inflate(this, resource, null);
int[] padding = {target.getPaddingLeft(), target.getPaddingTop(), target.getPaddingRight(), target.getPaddingBottom()};
target.setTextColor(tv.getTextColors().getDefaultColor());
target.setTypeface(tv.getTypeface());
target.setBackgroundDrawable(tv.getBackground());
target.setPadding(padding[0], padding[1], padding[2], padding[3]);
}
/**
* The right-hand side of the title associated with this activity.
* <p/>
* This will update dynamically as the activity loads/updates, but if
* it will ever have a value it must return a blank string when one
* isn't available.
*/
protected String getActivityTitle() {
return null;
}
public static String getTopLevelTitleName(Context c) {
try {
return Localization.get("app.display.name");
} catch (NoLocalizedTextException nlte) {
return c.getString(org.commcare.dalvik.R.string.title_bar_name);
}
}
protected static String getTitle(Context c, String local) {
String topLevel = getTopLevelTitleName(c);
String[] stepTitles = new String[0];
try {
stepTitles = CommCareApplication.instance().getCurrentSession().getHeaderTitles();
//See if we can insert any case hacks
int i = 0;
for (StackFrameStep step : CommCareApplication.instance().getCurrentSession().getFrame().getSteps()) {
try {
if (SessionFrame.STATE_DATUM_VAL.equals(step.getType())) {
//Haaack
if (step.getId() != null && step.getId().contains("case_id")) {
ACase foundCase = CommCareApplication.instance().getUserStorage(ACase.STORAGE_KEY, ACase.class).getRecordForValue(ACase.INDEX_CASE_ID, step.getValue());
stepTitles[i] = Localization.get("title.datum.wrapper", new String[]{foundCase.getName()});
}
}
} catch (Exception e) {
//TODO: Your error handling is bad and you should feel bad
}
++i;
}
} catch (SessionStateUninitException e) {
}
StringBuilder titleBuf = new StringBuilder(topLevel);
for (String title : stepTitles) {
if (title != null) {
titleBuf.append(" > ").append(title);
}
}
if (local != null) {
titleBuf.append(" > ").append(local);
}
return titleBuf.toString();
}
protected boolean isNetworkNotConnected() {
return !ConnectivityStatus.isNetworkAvailable(this);
}
// region - All methods for implementation of DialogController
@Override
public void updateProgress(String newMessage, String newTitle, int taskId) {
updateDialogContent(newMessage, newTitle, taskId);
}
@Override
public void updateProgress(String newMessage, int taskId) {
updateDialogContent(newMessage, null, taskId);
}
private void updateDialogContent(String newMessage, String newTitle, int taskId) {
CustomProgressDialog mProgressDialog = getCurrentProgressDialog();
if (mProgressDialog != null && !areFragmentsPaused) {
if (mProgressDialog.getTaskId() == taskId) {
mProgressDialog.updateMessage(newMessage);
if (newTitle != null) {
mProgressDialog.updateTitle(newTitle);
}
} else {
warnInvalidProgressUpdate(taskId);
}
}
}
@Override
public void hideTaskCancelButton() {
CustomProgressDialog mProgressDialog = getCurrentProgressDialog();
if (mProgressDialog != null) {
mProgressDialog.removeCancelButton();
}
}
@Override
public void updateProgressBarVisibility(boolean visible) {
CustomProgressDialog mProgressDialog = getCurrentProgressDialog();
if (mProgressDialog != null && !areFragmentsPaused) {
mProgressDialog.updateProgressBarVisibility(visible);
}
}
@Override
public void updateProgressBar(int progress, int max, int taskId) {
CustomProgressDialog mProgressDialog = getCurrentProgressDialog();
if (mProgressDialog != null && !areFragmentsPaused) {
if (mProgressDialog.getTaskId() == taskId) {
mProgressDialog.updateProgressBar(progress, max);
} else {
warnInvalidProgressUpdate(taskId);
}
}
}
private void warnInvalidProgressUpdate(int taskId) {
String message = "Attempting to update a progress dialog whose taskId (" + taskId +
" does not match the task for which the update message was intended.";
if(invalidTaskIdMessageThrown != taskId) {
invalidTaskIdMessageThrown = taskId;
Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION, message);
} else {
Log.w(TAG, message);
}
}
@Override
public void showProgressDialog(int taskId) {
if (taskId >= 0) {
CustomProgressDialog dialog = generateProgressDialog(taskId);
if (dialog != null) {
dialog.show(getSupportFragmentManager(), KEY_PROGRESS_DIALOG_FRAG);
}
}
}
@Override
public CustomProgressDialog getCurrentProgressDialog() {
return (CustomProgressDialog) getSupportFragmentManager().
findFragmentByTag(KEY_PROGRESS_DIALOG_FRAG);
}
@Override
public void dismissProgressDialog() {
CustomProgressDialog progressDialog = getCurrentProgressDialog();
if (progressDialog != null && progressDialog.isAdded()) {
if (areFragmentsPaused) {
triedDismissingWhilePaused = true;
} else {
progressDialog.dismiss();
}
}
}
@Override
public CustomProgressDialog generateProgressDialog(int taskId) {
//dummy method for compilation, implementation handled in those subclasses that need it
return null;
}
@Override
public AlertDialogFragment getCurrentAlertDialog() {
return (AlertDialogFragment) getSupportFragmentManager().
findFragmentByTag(KEY_ALERT_DIALOG_FRAG);
}
@Override
public void showPendingAlertDialog() {
if (alertDialogToShowOnResume != null) {
alertDialogToShowOnResume.show(getSupportFragmentManager(), KEY_ALERT_DIALOG_FRAG);
alertDialogToShowOnResume = null;
} else {
showPendingUserMessage();
}
}
@Override
public void dismissAlertDialog() {
DialogFragment alertDialog = getCurrentAlertDialog();
if (alertDialog != null) {
alertDialog.dismiss();
}
}
@Override
public void showAlertDialog(CommCareAlertDialog d) {
AlertDialogFragment dialog = AlertDialogFragment.fromCommCareAlertDialog(d);
if (areFragmentsPaused) {
alertDialogToShowOnResume = dialog;
} else {
if (getCurrentAlertDialog() != null) {
// replace existing dialog by dismissing it
dismissAlertDialog();
}
dialog.show(getSupportFragmentManager(), KEY_ALERT_DIALOG_FRAG);
}
}
// endregion
public Pair<Detail, TreeReference> requestEntityContext() {
return null;
}
/**
* Interface to perform additional setup code when adding an ActionBar
*/
public interface ActionBarInstantiator {
void onActionBarFound(MenuItem searchItem, SearchView searchView, MenuItem barcodeItem);
}
/**
* Tries to add a SearchView action to the app bar of the current Activity. If it is added,
* the alternative search widget is removed, and ActionBarInstantiator is run, if it exists.
* Used in EntitySelectActivity and FormRecordListActivity.
*
* @param activity Current activity
* @param menu Menu passed through onCreateOptionsMenu
* @param instantiator Optional ActionBarInstantiator for additional setup code.
*/
protected void tryToAddSearchActionToAppBar(Activity activity, Menu menu,
ActionBarInstantiator instantiator) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
MenuInflater inflater = activity.getMenuInflater();
inflater.inflate(org.commcare.dalvik.R.menu.action_bar_search_view, menu);
MenuItem searchMenuItem = menu.findItem(org.commcare.dalvik.R.id.search_action_bar);
SearchView searchView =
(SearchView) searchMenuItem.getActionView();
MenuItem barcodeItem = menu.findItem(org.commcare.dalvik.R.id.barcode_scan_action_bar);
if (searchView != null) {
int[] searchViewStyle =
AndroidUtil.getThemeColorIDs(this,
new int[]{org.commcare.dalvik.R.attr.searchbox_action_bar_color});
int id = searchView.getContext()
.getResources()
.getIdentifier("android:id/search_src_text", null, null);
TextView textView = (TextView) searchView.findViewById(id);
textView.setTextColor(searchViewStyle[0]);
if (instantiator != null) {
instantiator.onActionBarFound(searchMenuItem, searchView, barcodeItem);
}
}
View bottomSearchWidget = activity.findViewById(org.commcare.dalvik.R.id.searchfooter);
if (bottomSearchWidget != null) {
bottomSearchWidget.setVisibility(View.GONE);
}
}
}
/**
* Whether or not the "Back" action makes sense for this activity.
*
* @return True if "Back" is a valid concept for the Activity and should be shown
* in the action bar if available. False otherwise.
*/
public boolean isBackEnabled() {
return true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent mv) {
return !(mGestureDetector == null || !mGestureDetector.onTouchEvent(mv)) || super.dispatchTouchEvent(mv);
}
@Override
public boolean onDown(MotionEvent arg0) {
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (isHorizontalSwipe(this, e1, e2) && !isMainScreenBlocked) {
if (velocityX <= 0) {
return onForwardSwipe();
}
return onBackwardSwipe();
}
return false;
}
/**
* Action to take when user swipes forward during activity.
*
* @return Whether or not the swipe was handled
*/
protected boolean onForwardSwipe() {
return false;
}
/**
* Action to take when user swipes backward during activity.
*
* @return Whether or not the swipe was handled
*/
protected boolean onBackwardSwipe() {
return false;
}
@Override
public void onBackPressed() {
super.onBackPressed();
AudioController.INSTANCE.releaseCurrentMediaEntity();
}
@Override
public void onLongPress(MotionEvent arg0) {
// ignore
}
@Override
public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
return false;
}
@Override
public void onShowPress(MotionEvent arg0) {
// ignore
}
@Override
public boolean onSingleTapUp(MotionEvent arg0) {
return false;
}
/**
* Decide if two given MotionEvents represent a swipe.
*
* @return True iff the movement is a definitive horizontal swipe.
*/
private static boolean isHorizontalSwipe(Activity activity, MotionEvent e1, MotionEvent e2) {
DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
//details of the motion itself
float xMov = Math.abs(e1.getX() - e2.getX());
float yMov = Math.abs(e1.getY() - e2.getY());
double angleOfMotion = ((Math.atan(yMov / xMov) / Math.PI) * 180);
// for all screens a swipe is left/right of at least .25" and at an angle of no more than 30
//degrees
int xPixelLimit = (int) (dm.xdpi * .25);
return xMov > xPixelLimit && angleOfMotion < 30;
}
/**
* Rebuild the activity's menu options based on the current state of the activity.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void rebuildOptionsMenu() {
if (CommCareApplication.instance().getCurrentApp() != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
invalidateOptionsMenu();
} else {
supportInvalidateOptionsMenu();
}
}
}
public Spannable localize(String key) {
return MarkupUtil.localizeStyleSpannable(this, key);
}
public Spannable localize(String key, String arg) {
return MarkupUtil.localizeStyleSpannable(this, key, arg);
}
public Spannable localize(String key, String[] args) {
return MarkupUtil.localizeStyleSpannable(this, key, args);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void refreshActionBar() {
if (shouldShowBreadcrumbBar()) {
FragmentManager fm = this.getSupportFragmentManager();
BreadcrumbBarFragment bar = (BreadcrumbBarFragment) fm.findFragmentByTag("breadcrumbs");
bar.refresh(this);
}
}
/**
* Activity has been put in the background. Useful in knowing when to not
* perform dialog or fragment transactions
*/
protected boolean areFragmentsPaused() {
return areFragmentsPaused;
}
public void setMainScreenBlocked(boolean isBlocked) {
isMainScreenBlocked = isBlocked;
}
private boolean usesUIController() {
return this instanceof WithUIController;
}
public Object getUIManager() {
if (usesUIController()) {
return ((WithUIController)this).getUIController();
} else {
return this;
}
}
private boolean isManagedUiActivity() {
return ManagedUiFramework.isManagedUi(getUIManager().getClass());
}
public void setStateHolder(TaskConnectorFragment<R> stateHolder) {
this.stateHolder = stateHolder;
}
protected String getLastQueryString() {
return lastQueryString;
}
protected void setLastQueryString(String lastQueryString) {
this.lastQueryString = lastQueryString;
}
protected boolean shouldShowBreadcrumbBar() {
return true;
}
}