@Override
  protected EncounterAddFailedEvent doInBackground(Void... params) {
    RequestFuture<Encounter> encounterFuture = RequestFuture.newFuture();

    mServer.addEncounter(mPatient, mEncounter, encounterFuture, encounterFuture);
    Encounter encounter;
    try {
      encounter = encounterFuture.get();
    } catch (InterruptedException e) {
      return new EncounterAddFailedEvent(EncounterAddFailedEvent.Reason.INTERRUPTED, e);
    } catch (ExecutionException e) {
      LOG.e(e, "Server error while adding encounter");

      EncounterAddFailedEvent.Reason reason = EncounterAddFailedEvent.Reason.UNKNOWN_SERVER_ERROR;
      if (e.getCause() != null) {
        String errorMessage = e.getCause().getMessage();
        if (errorMessage.contains("failed to validate")) {
          reason = EncounterAddFailedEvent.Reason.FAILED_TO_VALIDATE;
        } else if (errorMessage.contains("Privileges required")) {
          reason = EncounterAddFailedEvent.Reason.FAILED_TO_AUTHENTICATE;
        }
      }
      LOG.e("Error response: %s", ((VolleyError) e.getCause()).networkResponse);

      return new EncounterAddFailedEvent(reason, (VolleyError) e.getCause());
    }

    if (encounter.uuid == null) {
      LOG.e(
          "Although the server reported an encounter successfully added, it did not "
              + "return a UUID for that encounter. This indicates a server error.");

      return new EncounterAddFailedEvent(
          EncounterAddFailedEvent.Reason.FAILED_TO_SAVE_ON_SERVER, null /*exception*/);
    }

    AppEncounter appEncounter = AppEncounter.fromNet(mPatient.uuid, encounter);

    if (appEncounter.observations.length > 0) {
      int inserted =
          mContentResolver.bulkInsert(
              Contracts.Observations.CONTENT_URI, appEncounter.toContentValuesArray());

      if (inserted != appEncounter.observations.length) {
        LOG.w(
            "Inserted %d observations for encounter. Expected: %d",
            inserted, appEncounter.observations.length);
        return new EncounterAddFailedEvent(
            EncounterAddFailedEvent.Reason.INVALID_NUMBER_OF_OBSERVATIONS_SAVED,
            null /*exception*/);
      }
    } else {
      LOG.w("Encounter was sent to the server but contained no observations.");
    }

    mUuid = encounter.uuid;

    return null;
  }
  @Override
  protected void onPostExecute(EncounterAddFailedEvent event) {
    // If an error occurred, post the error event.
    if (event != null) {
      mBus.post(event);
      return;
    }

    // If the UUID was not set, a programming error occurred. Log and post an error event.
    if (mUuid == null) {
      LOG.e(
          "Although an encounter add ostensibly succeeded, no UUID was set for the newly-"
              + "added encounter. This indicates a programming error.");

      mBus.post(
          new EncounterAddFailedEvent(EncounterAddFailedEvent.Reason.UNKNOWN, null /*exception*/));
      return;
    }

    // Otherwise, start a fetch task to fetch the encounter from the database.
    mBus.register(new CreationEventSubscriber());
    FetchSingleAsyncTask<AppEncounter> task =
        mTaskFactory.newFetchSingleAsyncTask(
            Contracts.Observations.CONTENT_URI,
            ENCOUNTER_PROJECTION,
            new EncounterUuidFilter(),
            mUuid,
            new AppEncounterConverter(mPatient.uuid),
            mBus);
    task.execute();
  }
  /** Starts a full sync. */
  public static void startFullSync() {
    Bundle b = new Bundle();
    // Request aggressively that the sync should start straight away.
    b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);

    // Fetch everything, except fetch only newly added observations if so enabled.
    b.putBoolean(SyncOption.FULL_SYNC.name(), true);
    LOG.i("Requesting full sync");
    ContentResolver.requestSync(getAccount(), Contracts.CONTENT_AUTHORITY, b);
  }
  /** Starts an sync of just the observations. */
  public static void startObservationsSync() {
    // Start by canceling any existing syncs, which may delay this one.
    ContentResolver.cancelSync(getAccount(), Contracts.CONTENT_AUTHORITY);

    Bundle b = new Bundle();
    // Request aggressively that the sync should start straight away.
    b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);

    // Fetch just the newly added observations.
    b.putBoolean(SyncPhase.SYNC_OBSERVATIONS.name(), true);
    b.putBoolean(SyncPhase.SYNC_ORDERS.name(), true);
    LOG.i("Requesting incremental observation sync");
    ContentResolver.requestSync(getAccount(), Contracts.CONTENT_AUTHORITY, b);
  }
/**
 * An {@link AsyncTask} that adds a patient encounter to a server.
 *
 * <p>If the operation succeeds, a {@link SingleItemCreatedEvent} is posted on the given {@link
 * CrudEventBus} with the added encounter. If the operation fails, a {@link EncounterAddFailedEvent}
 * is posted instead.
 */
public class AppAddEncounterAsyncTask extends AsyncTask<Void, Void, EncounterAddFailedEvent> {
  // TODO: Factor out common code between this class and AppAddPatientAsyncTask.
  private static final Logger LOG = Logger.create();

  private static final String[] ENCOUNTER_PROJECTION =
      new String[] {
        Contracts.ObservationColumns.CONCEPT_UUID,
        Contracts.ObservationColumns.ENCOUNTER_TIME,
        Contracts.ObservationColumns.ENCOUNTER_UUID,
        Contracts.ObservationColumns.PATIENT_UUID,
        Contracts.ObservationColumns.VALUE
      };

  private final AppAsyncTaskFactory mTaskFactory;
  private final AppTypeConverters mConverters;
  private final Server mServer;
  private final ContentResolver mContentResolver;
  private final AppPatient mPatient;
  private final AppEncounter mEncounter;
  private final CrudEventBus mBus;

  private String mUuid;

  /** Creates a new {@link AppAddEncounterAsyncTask}. */
  public AppAddEncounterAsyncTask(
      AppAsyncTaskFactory taskFactory,
      AppTypeConverters converters,
      Server server,
      ContentResolver contentResolver,
      AppPatient patient,
      AppEncounter encounter,
      CrudEventBus bus) {
    mTaskFactory = taskFactory;
    mConverters = converters;
    mServer = server;
    mContentResolver = contentResolver;
    mPatient = patient;
    mEncounter = encounter;
    mBus = bus;
  }

  @Override
  protected EncounterAddFailedEvent doInBackground(Void... params) {
    RequestFuture<Encounter> encounterFuture = RequestFuture.newFuture();

    mServer.addEncounter(mPatient, mEncounter, encounterFuture, encounterFuture);
    Encounter encounter;
    try {
      encounter = encounterFuture.get();
    } catch (InterruptedException e) {
      return new EncounterAddFailedEvent(EncounterAddFailedEvent.Reason.INTERRUPTED, e);
    } catch (ExecutionException e) {
      LOG.e(e, "Server error while adding encounter");

      EncounterAddFailedEvent.Reason reason = EncounterAddFailedEvent.Reason.UNKNOWN_SERVER_ERROR;
      if (e.getCause() != null) {
        String errorMessage = e.getCause().getMessage();
        if (errorMessage.contains("failed to validate")) {
          reason = EncounterAddFailedEvent.Reason.FAILED_TO_VALIDATE;
        } else if (errorMessage.contains("Privileges required")) {
          reason = EncounterAddFailedEvent.Reason.FAILED_TO_AUTHENTICATE;
        }
      }
      LOG.e("Error response: %s", ((VolleyError) e.getCause()).networkResponse);

      return new EncounterAddFailedEvent(reason, (VolleyError) e.getCause());
    }

    if (encounter.uuid == null) {
      LOG.e(
          "Although the server reported an encounter successfully added, it did not "
              + "return a UUID for that encounter. This indicates a server error.");

      return new EncounterAddFailedEvent(
          EncounterAddFailedEvent.Reason.FAILED_TO_SAVE_ON_SERVER, null /*exception*/);
    }

    AppEncounter appEncounter = AppEncounter.fromNet(mPatient.uuid, encounter);

    if (appEncounter.observations.length > 0) {
      int inserted =
          mContentResolver.bulkInsert(
              Contracts.Observations.CONTENT_URI, appEncounter.toContentValuesArray());

      if (inserted != appEncounter.observations.length) {
        LOG.w(
            "Inserted %d observations for encounter. Expected: %d",
            inserted, appEncounter.observations.length);
        return new EncounterAddFailedEvent(
            EncounterAddFailedEvent.Reason.INVALID_NUMBER_OF_OBSERVATIONS_SAVED,
            null /*exception*/);
      }
    } else {
      LOG.w("Encounter was sent to the server but contained no observations.");
    }

    mUuid = encounter.uuid;

    return null;
  }

  @Override
  protected void onPostExecute(EncounterAddFailedEvent event) {
    // If an error occurred, post the error event.
    if (event != null) {
      mBus.post(event);
      return;
    }

    // If the UUID was not set, a programming error occurred. Log and post an error event.
    if (mUuid == null) {
      LOG.e(
          "Although an encounter add ostensibly succeeded, no UUID was set for the newly-"
              + "added encounter. This indicates a programming error.");

      mBus.post(
          new EncounterAddFailedEvent(EncounterAddFailedEvent.Reason.UNKNOWN, null /*exception*/));
      return;
    }

    // Otherwise, start a fetch task to fetch the encounter from the database.
    mBus.register(new CreationEventSubscriber());
    FetchSingleAsyncTask<AppEncounter> task =
        mTaskFactory.newFetchSingleAsyncTask(
            Contracts.Observations.CONTENT_URI,
            ENCOUNTER_PROJECTION,
            new EncounterUuidFilter(),
            mUuid,
            new AppEncounterConverter(mPatient.uuid),
            mBus);
    task.execute();
  }

  // After updating an encounter, we fetch the encounter from the database. The result of the
  // fetch determines if adding a patient was truly successful and propagates a new event to
  // report success/failure.
  @SuppressWarnings("unused") // Called by reflection from EventBus.
  private final class CreationEventSubscriber {
    public void onEventMainThread(SingleItemFetchedEvent<AppEncounter> event) {
      mBus.post(new SingleItemCreatedEvent<>(event.item));
      mBus.unregister(this);
    }

    public void onEventMainThread(SingleItemFetchFailedEvent event) {
      mBus.post(
          new EncounterAddFailedEvent(
              EncounterAddFailedEvent.Reason.FAILED_TO_FETCH_SAVED_OBSERVATION,
              new Exception(event.error)));
      mBus.unregister(this);
    }
  }
}
/** Functional tests for {@link PatientChartActivity}. */
@MediumTest
public class PatientChartActivityTest extends FunctionalTestCase {
  private static final Logger LOG = Logger.create();

  private static final int ROW_HEIGHT = 84;

  public PatientChartActivityTest() {
    super();
  }

  /** Tests that the general condition dialog successfully changes general condition. */
  public void testGeneralConditionDialog_AppliesGeneralConditionChange() {
    inUserLoginGoToDemoPatientChart();
    onView(withId(R.id.patient_chart_vital_general_parent)).perform(click());
    screenshot("General Condition Dialog");
    onView(withText(R.string.status_well)).perform(click());

    // Wait for a sync operation to update the chart.
    EventBusIdlingResource<SyncFinishedEvent> syncFinishedIdlingResource =
        new EventBusIdlingResource<>(UUID.randomUUID().toString(), mEventBus);
    Espresso.registerIdlingResources(syncFinishedIdlingResource);

    // Check for updated vital view.
    checkViewDisplayedSoon(withText(R.string.status_well));

    // Check for updated chart view.
    onView(
            allOf(
                withText(R.string.status_short_desc_well),
                not(withId(R.id.patient_chart_vital_general_condition_number))))
        .check(matches(isDisplayed()));
  }

  /** Tests that the encounter form can be opened more than once. */
  public void testPatientChart_CanOpenEncounterFormMultipleTimes() {
    inUserLoginGoToDemoPatientChart();
    // Load the chart once
    openEncounterForm();

    // Dismiss
    onView(withText("Discard")).perform(click());

    // Load the chart again
    openEncounterForm();

    // Dismiss
    onView(withText("Discard")).perform(click());
  }

  /**
   * Tests that the admission date is correctly displayed in the header. TODO/completeness:
   * Currently disabled. Re-enable once date picker selection works (supposedly works in Espresso
   * 2.0).
   */
  /*public void testPatientChart_ShowsCorrectAdmissionDate() {
      mDemoPatient.admissionDate = Optional.of(DateTime.now().minusDays(5));
      inUserLoginGoToDemoPatientChart();
      onView(allOf(
              isDescendantOfA(withId(R.id.attribute_admission_days)),
              withText("Day 6")))
              .check(matches(isDisplayed()));
      screenshot("Patient Chart");
  }*/

  /**
   * Tests that the patient chart shows the correct symptoms onset date. TODO/completeness:
   * Currently disabled. Re-enable once date picker selection works (supposedly works in Espresso
   * 2.0).
   */
  /*public void testPatientChart_ShowsCorrectSymptomsOnsetDate() {
      inUserLoginGoToDemoPatientChart();
      onView(allOf(
              isDescendantOfA(withId(R.id.attribute_symptoms_onset_days)),
              withText("Day 8")))
              .check(matches(isDisplayed()));
      screenshot("Patient Chart");
  }*/

  /**
   * Tests that the patient chart shows all days, even when no observations are present.
   * TODO/completeness: Currently disabled. Re-enable once date picker selection works (supposedly
   * works in Espresso 2.0).
   */
  /*public void testPatientChart_ShowsAllDaysInChartWhenNoObservations() {
      inUserLoginGoToDemoPatientChart();
      onView(withText(containsString("Today (Day 6)"))).check(matchesWithin(isDisplayed(), 5000));
      screenshot("Patient Chart");
  }*/

  // TODO/completeness: Disabled as there seems to be no easy way of
  // scrolling correctly with no adapter view.
  /** Tests that encounter time can be set to a date in the past and still displayed correctly. */
  /*public void testCanSubmitObservationsInThePast() {
      inUserLoginGoToDemoPatientChart();
      openEncounterForm();
      selectDateFromDatePicker("2015", "Jan", null);
      answerVisibleTextQuestion("Temperature", "29.1");
      saveForm();
      checkObservationValueEquals(0, "29.1", "1 Jan"); // Temperature
  }*/

  /** Tests that dismissing a form immediately closes it if no changes have been made. */
  public void testDismissButtonReturnsImmediatelyWithNoChanges() {
    inUserLoginGoToDemoPatientChart();
    openEncounterForm();
    discardForm();
  }

  /** Tests that dismissing a form results in a dialog if changes have been made. */
  public void testDismissButtonShowsDialogWithChanges() {
    inUserLoginGoToDemoPatientChart();
    openEncounterForm();
    answerVisibleTextQuestion("Temperature", "29.2");

    // Try to discard and give up.
    discardForm();
    onView(withText(R.string.title_discard_observations)).check(matches(isDisplayed()));
    onView(withText(R.string.no)).perform(click());

    // Try to discard and actually go back.
    discardForm();
    onView(withText(R.string.title_discard_observations)).check(matches(isDisplayed()));
    onView(withText(R.string.yes)).perform(click());
  }

  /** Tests that PCR submission does not occur without confirmation being specified. */
  public void testPcr_requiresConfirmation() {
    inUserLoginGoToDemoPatientChart();
    openPcrForm();
    answerVisibleTextQuestion("Ebola L gene", "38");
    answerVisibleTextQuestion("Ebola Np gene", "35");

    onView(withText("Save")).perform(click());

    // Saving form should not work (can't check for a Toast within Espresso)
    onView(withText(R.string.form_entry_save)).check(matches(isDisplayed()));

    // Try again with confirmation
    answerVisibleToggleQuestion("confirm this lab test result", "Confirm Lab Test Results");
    saveForm();

    // Check that new values displayed.
    checkViewDisplayedSoon(withText(containsString("38.0 / 35.0")));
  }

  /** Tests that PCR displays 'NEG' in place of numbers when 40.0 is specified. */
  public void testPcr_showsNegFor40() {
    inUserLoginGoToDemoPatientChart();
    openPcrForm();
    answerVisibleTextQuestion("Ebola L gene", "40");
    answerVisibleTextQuestion("Ebola Np gene", "40");
    answerVisibleToggleQuestion("confirm this lab test result", "Confirm Lab Test Results");
    saveForm();

    checkViewDisplayedSoon(withText(containsString("NEG / NEG")));
  }

  /**
   * Tests that, when multiple encounters for the same encounter time are submitted within a short
   * period of time, that only the latest encounter is present in the relevant column.
   */
  public void testEncounter_latestEncounterIsAlwaysShown() {
    inUserLoginGoToDemoPatientChart();

    // Update a vital tile (pulse) as well as a couple of observations (temperature, vomiting
    // count), and verify that the latest value is visible for each.
    for (int i = 0; i < 6; i++) {
      openEncounterForm();

      String pulse = Integer.toString(i + 80);
      String temp = Integer.toString(i + 35) + ".0";
      String vomiting = Integer.toString(5 - i);
      answerVisibleTextQuestion("Pulse", pulse);
      answerVisibleTextQuestion("Temperature", temp);
      answerVisibleTextQuestion("Vomiting", vomiting);
      saveForm();

      checkVitalValueContains("Pulse", pulse);
      checkObservationValueEquals(0 /*Temperature*/, temp, "Today");
      checkObservationValueEquals(6 /*Vomiting*/, vomiting, "Today");
    }
  }

  /** Ensures that non-overlapping observations for the same encounter are combined. */
  public void testCombinesNonOverlappingObservationsForSameEncounter() {
    inUserLoginGoToDemoPatientChart();
    // Enter first set of observations for this encounter.
    openEncounterForm();
    answerVisibleTextQuestion("Pulse", "74");
    answerVisibleTextQuestion("Respiratory rate", "23");
    answerVisibleTextQuestion("Temperature", "36");
    saveForm();
    // Enter second set of observations for this encounter.
    openEncounterForm();
    answerVisibleToggleQuestion("Signs and Symptoms", "Nausea");
    answerVisibleTextQuestion("Vomiting", "2");
    answerVisibleTextQuestion("Diarrhoea", "5");
    saveForm();

    // Check that all values are now visible.
    checkVitalValueContains("Pulse", "74");
    checkVitalValueContains("Respiration", "23");
    checkObservationValueEquals(0, "36.0", "Today"); // Temp
    checkObservationSet(5, "Today"); // Nausea
    checkObservationValueEquals(6, "2", "Today"); // Vomiting
    checkObservationValueEquals(7, "5", "Today"); // Diarrhoea
  }

  /** Exercises all fields in the encounter form, except for encounter time. */
  public void testEncounter_allFieldsWorkOtherThanEncounterTime() {
    // TODO/robustness: Get rid of magic numbers in these tests.
    inUserLoginGoToDemoPatientChart();
    openEncounterForm();
    answerVisibleTextQuestion("Pulse", "80");
    answerVisibleTextQuestion("Respiratory rate", "20");
    answerVisibleTextQuestion("Temperature", "31");
    answerVisibleTextQuestion("Weight", "90");
    answerVisibleToggleQuestion("Signs and Symptoms", "Nausea");
    answerVisibleTextQuestion("Vomiting", "4");
    answerVisibleTextQuestion("Diarrhoea", "6");
    answerVisibleToggleQuestion("Pain level", "Severe");
    answerVisibleToggleQuestion("Pain (Detail)", "Headache");
    answerVisibleToggleQuestion("Pain (Detail)", "Back pain");
    answerVisibleToggleQuestion("Bleeding", "Yes");
    answerVisibleToggleQuestion("Bleeding (Detail)", "Nosebleed");
    answerVisibleToggleQuestion("Weakness", "Moderate");
    answerVisibleToggleQuestion("Other Symptoms", "Red eyes");
    answerVisibleToggleQuestion("Other Symptoms", "Hiccups");
    answerVisibleToggleQuestion("Consciousness", "Responds to voice");
    answerVisibleToggleQuestion("Mobility", "Assisted");
    answerVisibleToggleQuestion("Diet", "Fluids");
    answerVisibleToggleQuestion("Hydration", "Needs ORS");
    answerVisibleToggleQuestion("Condition", "5");
    answerVisibleToggleQuestion("Additional Details", "Pregnant");
    answerVisibleToggleQuestion("Additional Details", "IV access present");
    answerVisibleTextQuestion("Notes", "possible malaria");
    saveForm();

    checkVitalValueContains("Pulse", "80");
    checkVitalValueContains("Respiration", "20");
    checkVitalValueContains("Consciousness", "Responds to voice");
    checkVitalValueContains("Mobility", "Assisted");
    checkVitalValueContains("Diet", "Fluids");
    checkVitalValueContains("Hydration", "Needs ORS");
    checkVitalValueContains("Condition", "5");
    checkVitalValueContains("Pain level", "Severe");

    checkObservationValueEquals(0, "31.0", "Today"); // Temp
    checkObservationValueEquals(1, "90", "Today"); // Weight
    checkObservationValueEquals(2, "5", "Today"); // Condition
    checkObservationValueEquals(3, "V", "Today"); // Consciousness
    checkObservationValueEquals(4, "As", "Today"); // Mobility
    checkObservationSet(5, "Today"); // Nausea
    checkObservationValueEquals(6, "4", "Today"); // Vomiting
    checkObservationValueEquals(7, "6", "Today"); // Diarrhoea
    checkObservationValueEquals(8, "3", "Today"); // Pain level
    checkObservationSet(9, "Today"); // Bleeding
    checkObservationValueEquals(10, "2", "Today"); // Weakness
    checkObservationSet(13, "Today"); // Hiccups
    checkObservationSet(14, "Today"); // Red eyes
    checkObservationSet(15, "Today"); // Headache
    checkObservationSet(21, "Today"); // Back pain
    checkObservationSet(24, "Today"); // Nosebleed

    onView(withText(containsString("Pregnant"))).check(matches(isDisplayed()));
    onView(withText(containsString("IV Fitted"))).check(matches(isDisplayed()));

    // TODO/completeness: exercise the Notes field
  }

  protected void openEncounterForm() {
    checkViewDisplayedSoon(withId(R.id.action_update_chart));
    EventBusIdlingResource<FetchXformSucceededEvent> xformIdlingResource =
        new EventBusIdlingResource<FetchXformSucceededEvent>(
            UUID.randomUUID().toString(), mEventBus);
    onView(withId(R.id.action_update_chart)).perform(click());
    Espresso.registerIdlingResources(xformIdlingResource);

    // Give the form time to be parsed on the client (this does not result in an event firing).
    checkViewDisplayedSoon(withText("Encounter"));
  }

  protected void openPcrForm() {
    EventBusIdlingResource<FetchXformSucceededEvent> xformIdlingResource =
        new EventBusIdlingResource<FetchXformSucceededEvent>(
            UUID.randomUUID().toString(), mEventBus);
    onView(withId(R.id.action_add_test_result)).perform(click());
    Espresso.registerIdlingResources(xformIdlingResource);

    // Give the form time to be parsed on the client (this does not result in an event firing).
    checkViewDisplayedSoon(withText("Encounter"));
  }

  private void discardForm() {
    onView(withText("Discard")).perform(click());
  }

  private void saveForm() {
    IdlingResource xformWaiter = getXformSubmissionIdlingResource();
    onView(withText("Save")).perform(click());
    Espresso.registerIdlingResources(xformWaiter);
  }

  private void answerVisibleTextQuestion(String questionText, String answerText) {
    onView(
            allOf(
                isAssignableFrom(EditText.class),
                hasSibling(
                    allOf(
                        isAssignableFrom(MediaLayout.class),
                        hasDescendant(
                            allOf(
                                isAssignableFrom(TextView.class),
                                withText(containsString(questionText))))))))
        .perform(scrollTo(), typeText(answerText));
  }

  private void answerVisibleToggleQuestion(String questionText, String answerText) {
    // Close the soft keyboard before answering any toggle questions -- on rare occasions,
    // if Espresso answers one of these questions and is then instructed to type into another
    // field, the input event will actually be generated as the keyboard is hiding and will be
    // lost, but Espresso won't detect this case.
    Espresso.closeSoftKeyboard();
    onView(
            allOf(
                anyOf(isAssignableFrom(CheckBox.class), isAssignableFrom(RadioButton.class)),
                isDescendantOfA(
                    allOf(
                        anyOf(
                            isAssignableFrom(ButtonsSelectOneWidget.class),
                            isAssignableFrom(TableWidgetGroup.class),
                            isAssignableFrom(ODKView.class)),
                        hasDescendant(withText(containsString(questionText))))),
                withText(containsString(answerText))))
        .perform(scrollTo(), click());
  }

  private void checkObservationValueEquals(int row, String value, String dateKey) {
    // TODO/completeness: actually check dateKey

    onView(
            allOf(
                withText(value),
                isDescendantOfA(inRow(row, ROW_HEIGHT)),
                isDescendantOfA(isAssignableFrom(DataGridView.LinkableRecyclerView.class))))
        .perform(scrollTo())
        .check(matches(isDisplayed()));
  }

  private void checkObservationSet(int row, String dateKey) {
    // TODO/completeness: actually check dateKey
    onView(
            allOf(
                isDescendantOfA(inRow(row, ROW_HEIGHT)),
                hasBackground(
                    getActivity().getResources().getDrawable(R.drawable.chart_cell_active)),
                isDescendantOfA(isAssignableFrom(DataGridView.LinkableRecyclerView.class))))
        .perform(scrollTo())
        .check(matches(isDisplayed()));
  }

  private void checkVitalValueContains(String vitalName, String vitalValue) {
    // Check for updated vital view.
    checkViewDisplayedSoon(
        allOf(
            withText(containsString(vitalValue)), hasSibling(withText(containsString(vitalName)))));
  }

  private IdlingResource getXformSubmissionIdlingResource() {
    return new EventBusIdlingResource<SubmitXformSucceededEvent>(
        UUID.randomUUID().toString(), mEventBus);
  }
}
/**
 * A {@link Service} that manages the app's sync account, including static functions for account
 * registration and sync requests. For a detailed description of the app's sync architecture, see:
 * https://github.com/projectbuendia/buendia/wiki/Client-Sync
 */
public class SyncAccountService extends Service {

  public static final String ACCOUNT_NAME = "sync";
  private static final Logger LOG = Logger.create();
  private static final long SYNC_PERIOD = 5 * 60; // 5 minutes (in seconds)
  @Inject static AppSettings sSettings;

  private Authenticator mAuthenticator;

  /** Sets up the sync account for this app. */
  public static void initialize(Context context) {
    if (createAccount(context) || !sSettings.getSyncAccountInitialized()) {
      startFullSync();
      sSettings.setSyncAccountInitialized(true);
    }
  }

  /**
   * Creates the sync account for this app if it doesn't already exist.
   *
   * @return true if a new account was created
   */
  private static boolean createAccount(Context context) {
    Account account = getAccount();
    AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE);
    if (accountManager.addAccountExplicitly(account, null, null)) {
      // Enable automatic sync for the account with a period of SYNC_PERIOD.
      ContentResolver.setIsSyncable(account, Contracts.CONTENT_AUTHORITY, 1);
      ContentResolver.setSyncAutomatically(account, Contracts.CONTENT_AUTHORITY, true);
      Bundle b = new Bundle();
      b.putBoolean(SyncOption.FULL_SYNC.name(), true);
      ContentResolver.addPeriodicSync(account, Contracts.CONTENT_AUTHORITY, b, SYNC_PERIOD);
      return true;
    }
    return false;
  }

  /** Starts a full sync. */
  public static void startFullSync() {
    Bundle b = new Bundle();
    // Request aggressively that the sync should start straight away.
    b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);

    // Fetch everything, except fetch only newly added observations if so enabled.
    b.putBoolean(SyncOption.FULL_SYNC.name(), true);
    LOG.i("Requesting full sync");
    ContentResolver.requestSync(getAccount(), Contracts.CONTENT_AUTHORITY, b);
  }

  /** Gets the app's sync account (call initialize() before using this). */
  public static Account getAccount() {
    return new Account(ACCOUNT_NAME, BuildConfig.ACCOUNT_TYPE);
  }

  /** Starts an sync of just the observations. */
  public static void startObservationsSync() {
    // Start by canceling any existing syncs, which may delay this one.
    ContentResolver.cancelSync(getAccount(), Contracts.CONTENT_AUTHORITY);

    Bundle b = new Bundle();
    // Request aggressively that the sync should start straight away.
    b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);

    // Fetch just the newly added observations.
    b.putBoolean(SyncPhase.SYNC_OBSERVATIONS.name(), true);
    b.putBoolean(SyncPhase.SYNC_ORDERS.name(), true);
    LOG.i("Requesting incremental observation sync");
    ContentResolver.requestSync(getAccount(), Contracts.CONTENT_AUTHORITY, b);
  }

  @Override
  public void onCreate() {
    LOG.i("Service created");
    mAuthenticator = new Authenticator(this);
  }

  @Override
  public void onDestroy() {
    LOG.i("Service destroyed");
  }

  @Override
  public IBinder onBind(Intent intent) {
    return mAuthenticator.getIBinder();
  }

  /** A dummy authenticator. */
  private static class Authenticator extends AbstractAccountAuthenticator {
    public Authenticator(Context context) {
      super(context);
    }

    public Bundle addAccount(
        AccountAuthenticatorResponse r, String s1, String s2, String[] ss, Bundle b) {
      return null;
    }

    public Bundle confirmCredentials(AccountAuthenticatorResponse r, Account a, Bundle b) {
      return null;
    }

    public Bundle editProperties(AccountAuthenticatorResponse r, String s) {
      throw new UnsupportedOperationException();
    }

    public Bundle getAuthToken(AccountAuthenticatorResponse r, Account a, String s, Bundle b) {
      throw new UnsupportedOperationException();
    }

    public String getAuthTokenLabel(String s) {
      throw new UnsupportedOperationException();
    }

    public Bundle updateCredentials(AccountAuthenticatorResponse r, Account a, String s, Bundle b) {
      throw new UnsupportedOperationException();
    }

    public Bundle hasFeatures(AccountAuthenticatorResponse r, Account a, String[] ss) {
      throw new UnsupportedOperationException();
    }
  }
}
 @Override
 public void onDestroy() {
   LOG.i("Service destroyed");
 }
 @Override
 public void onCreate() {
   LOG.i("Service created");
   mAuthenticator = new Authenticator(this);
 }