@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); }