// Copyright 2015 The Project Buendia Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at: http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distrib-
// uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// specific language governing permissions and limitations under the License.
package org.projectbuendia.client.models;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import org.joda.time.DateTime;
import org.projectbuendia.client.events.CleanupSubscriber;
import org.projectbuendia.client.events.CrudEventBus;
import org.projectbuendia.client.events.data.AppLocationTreeFetchedEvent;
import org.projectbuendia.client.events.data.ItemCreatedEvent;
import org.projectbuendia.client.events.data.ItemFetchedEvent;
import org.projectbuendia.client.events.data.ItemUpdatedEvent;
import org.projectbuendia.client.events.data.TypedCursorFetchedEvent;
import org.projectbuendia.client.events.data.TypedCursorFetchedEventFactory;
import org.projectbuendia.client.filter.db.SimpleSelectionFilter;
import org.projectbuendia.client.filter.db.patient.UuidFilter;
import org.projectbuendia.client.models.tasks.AddPatientTask;
import org.projectbuendia.client.models.tasks.UpdatePatientTask;
import org.projectbuendia.client.models.tasks.TaskFactory;
import org.projectbuendia.client.net.Server;
import org.projectbuendia.client.providers.Contracts;
import org.projectbuendia.client.utils.Logger;
import org.projectbuendia.client.utils.Utils;
import de.greenrobot.event.NoSubscriberEvent;
/**
* A model that manages all data access within the application.
* <p/>
* <p>This model's {@code fetch} methods often provide {@link TypedCursor}s as results, which MUST
* be closed when the consumer is done with them.
* <p/>
* <p>Updates done through this model are written through to a backing {@link Server}; callers do
* not need to worry about the implementation details of this.
*/
public class AppModel {
public static final String ORDER_EXECUTED_CONCEPT_UUID = "buendia-concept-order_executed";
public static final String CHART_UUID = "ea43f213-66fb-4af6-8a49-70fd6b9ce5d4";
private static final Logger LOG = Logger.create();
private final ContentResolver mContentResolver;
private final LoaderSet mLoaderSet;
private final TaskFactory mTaskFactory;
/**
* Returns true iff the model has previously been fully downloaded from the server--that is, if
* locations, patients, users, charts, and observations were all downloaded at some point. Note
* that this data may be out-of-date, but must be present in some form for proper operation of
* the app.
*/
public boolean isFullModelAvailable() {
return getLastFullSyncTime() != null;
}
public DateTime getLastFullSyncTime() {
// The sync process is transactional, but in rare cases, a sync may complete without ever
// having started--this is the case if user data is cleared mid-sync, for example. To check
// that a sync actually completed, we look at the FULL_SYNC_START_MILLIS and
// FULL_SYNC_END_MILLIS columns in the Misc table, which are written to as the first and
// last operations of a complete sync. If both of these fields are present, and the last
// end time is greater than the last start time, then a full sync must have completed.
try (Cursor c = mContentResolver.query(
Contracts.Misc.CONTENT_URI, null, null, null, null)) {
LOG.d("Sync timing result count: %d", c.getCount());
if (c.moveToNext()) {
DateTime fullSyncStart = Utils.getDateTime(c, Contracts.Misc.FULL_SYNC_START_MILLIS);
DateTime fullSyncEnd = Utils.getDateTime(c, Contracts.Misc.FULL_SYNC_END_MILLIS);
LOG.i("full_sync_start_millis = %s, full_sync_end_millis = %s",
fullSyncStart, fullSyncEnd);
if (fullSyncStart != null && fullSyncEnd != null && fullSyncEnd.isAfter(fullSyncStart)) {
return fullSyncEnd;
}
}
return null;
}
}
public void VoidObservation(CrudEventBus bus, VoidObs voidObs) {
String conditions = Contracts.Observations.UUID + " = ?";
ContentValues values = new ContentValues();
values.put(Contracts.Observations.VOIDED,1);
mContentResolver.update(Contracts.Observations.CONTENT_URI, values, conditions, new String[]{voidObs.Uuid});
mTaskFactory.voidObsTask(bus, voidObs).execute();
}
/**
* Asynchronously fetches all locations as a tree, posting an
* {@link AppLocationTreeFetchedEvent} on the specified event bus when complete.
*/
public void fetchLocationTree(CrudEventBus bus, String locale) {
bus.registerCleanupSubscriber(new CrudEventBusCleanupSubscriber(bus));
new FetchLocationTreeAsyncTask(
mContentResolver, locale, mLoaderSet.locationLoader, bus).execute();
}
/** Asynchronously downloads one patient from the server and saves it locally. */
public void downloadSinglePatient(CrudEventBus bus, String patientId) {
bus.registerCleanupSubscriber(new CrudEventBusCleanupSubscriber(bus));
mTaskFactory.newDownloadSinglePatientTask(patientId, bus).execute();
}
/**
* Asynchronously fetches patients, posting a {@link TypedCursorFetchedEvent} with
* {@link Patient}s on the specified event bus when complete.
*/
public void fetchPatients(CrudEventBus bus, SimpleSelectionFilter filter, String constraint) {
bus.registerCleanupSubscriber(new CrudEventBusCleanupSubscriber(bus));
// NOTE: We need to keep the object creation separate from calling #execute() here, because
// the type inference breaks on Java 8 otherwise, which throws
// `java.lang.ClassCastException: java.lang.Object[] cannot be cast to java.lang.Void[]`.
// See http://stackoverflow.com/questions/24136126/fatal-exception-asynctask and
// https://github.com/projectbuendia/client/issues/7
FetchTypedCursorAsyncTask<Patient> task = new FetchTypedCursorAsyncTask<>(
Contracts.Patients.CONTENT_URI,
// The projection must contain an "_id" column for the ListAdapter as well as all
// the columns used in Patient.Loader.fromCursor().
null, //new String[] {"rowid as _id", Patients.UUID, Patients.ID, Patients.GIVEN_NAME,
//Patients.FAMILY_NAME, Patients.BIRTHDATE, Patients.GENDER, Patients.LOCATION_UUID},
Patient.class, mContentResolver,
filter, constraint, mLoaderSet.patientLoader, bus);
task.execute();
}
/**
* Asynchronously fetches a single patient by UUID, posting a {@link ItemFetchedEvent}
* with the {@link Patient} on the specified event bus when complete.
*/
public void fetchSinglePatient(CrudEventBus bus, String uuid) {
mTaskFactory.newFetchItemTask(
Contracts.Patients.CONTENT_URI, null, new UuidFilter(), uuid,
mLoaderSet.patientLoader, bus).execute();
}
/**
* Asynchronously fetches patients, posting a {@link TypedCursorFetchedEvent} with
* {@link User}s on the specified event bus when complete.
*/
public void fetchUsers(CrudEventBus bus) {
// Register for error events so that we can close cursors if we need to.
bus.registerCleanupSubscriber(new CrudEventBusCleanupSubscriber(bus));
// TODO: Asynchronously fetch users or delete this function.
}
/**
* Asynchronously adds a patient, posting a
* {@link ItemCreatedEvent} with the newly-added patient on
* the specified event bus when complete.
*/
public void addPatient(CrudEventBus bus, PatientDelta patientDelta) {
AddPatientTask task = mTaskFactory.newAddPatientTask(patientDelta, bus);
task.execute();
}
/**
* Asynchronously updates a patient, posting a
* {@link ItemUpdatedEvent} with the updated
* {@link Patient} on the specified event bus when complete.
*/
public void updatePatient(
CrudEventBus bus, String patientUuid, PatientDelta patientDelta) {
UpdatePatientTask task =
mTaskFactory.newUpdatePatientTask(patientUuid, patientDelta, bus);
task.execute();
}
/**
* Asynchronously adds or updates an order (depending whether order.uuid is null), posting an
* {@link ItemCreatedEvent} or {@link ItemUpdatedEvent} when complete.
*/
public void saveOrder(CrudEventBus bus, Order order) {
mTaskFactory.newSaveOrderTask(order, bus).execute();
}
/** Asynchronously deletes an order. */
public void deleteOrder(CrudEventBus bus, String orderUuid) {
mTaskFactory.newDeleteOrderTask(orderUuid, bus).execute();
}
/**
* Asynchronously adds an encounter that records an order as executed, posting a
* {@link ItemCreatedEvent} when complete.
*/
public void addOrderExecutedEncounter(CrudEventBus bus, Patient patient, String orderUuid) {
addEncounter(bus, patient, new Encounter(
patient.uuid, null, DateTime.now(), null, new String[]{orderUuid}
));
}
/**
* Asynchronously adds an encounter to a patient, posting a
* {@link ItemCreatedEvent} when complete.
*/
public void addEncounter(CrudEventBus bus, Patient patient, Encounter encounter) {
mTaskFactory.newAddEncounterTask(patient, encounter, bus).execute();
}
AppModel(ContentResolver contentResolver,
LoaderSet loaderSet,
TaskFactory taskFactory) {
mContentResolver = contentResolver;
mLoaderSet = loaderSet;
mTaskFactory = taskFactory;
}
public void voidObservation(CrudEventBus bus, VoidObs obs) {
mTaskFactory.newVoidObsAsyncTask(obs, bus).execute();
}
/** A subscriber that handles error events posted to {@link CrudEventBus}es. */
private static class CrudEventBusCleanupSubscriber implements CleanupSubscriber {
private final CrudEventBus mBus;
public CrudEventBusCleanupSubscriber(CrudEventBus bus) {
mBus = bus;
}
@Override @SuppressWarnings("unused") // Called by reflection from event bus.
public void onEvent(NoSubscriberEvent event) {
if (event.originalEvent instanceof TypedCursorFetchedEvent<?>) {
// If no subscribers were registered for a DataFetchedEvent, then the TypedCursor in
// the event won't be managed by anyone else; therefore, we close it ourselves.
((TypedCursorFetchedEvent<?>) event.originalEvent).cursor.close();
} else if (event.originalEvent instanceof AppLocationTreeFetchedEvent) {
((AppLocationTreeFetchedEvent) event.originalEvent).tree.close();
}
mBus.unregisterCleanupSubscriber(this);
}
@Override public void onAllUnregistered() {
mBus.unregisterCleanupSubscriber(this);
}
}
private static class FetchLocationTreeAsyncTask extends AsyncTask<Void, Void, LocationTree> {
private final ContentResolver mContentResolver;
private final String mLocale;
private final CursorLoader<Location> mLoader;
private final CrudEventBus mBus;
public FetchLocationTreeAsyncTask(
ContentResolver contentResolver,
String locale,
CursorLoader<Location> loader,
CrudEventBus bus) {
mContentResolver = contentResolver;
mLocale = locale;
mLoader = loader;
mBus = bus;
}
@Override protected LocationTree doInBackground(Void... voids) {
Cursor cursor = null;
try {
// TODO: Ensure this cursor is closed. A straightforward try/finally doesn't do the
// job here because the cursor has to stay open for the TypedCursor to work.
cursor = mContentResolver.query(
Contracts.getLocalizedLocationsUri(mLocale), null, null, null, null);
return LocationTree.forTypedCursor(new TypedCursorWithLoader<>(cursor, mLoader));
} catch (Exception e) {
if (cursor != null) {
cursor.close();
}
throw e;
}
}
@Override protected void onPostExecute(LocationTree result) {
mBus.post(new AppLocationTreeFetchedEvent(result));
}
}
private static class FetchTypedCursorAsyncTask<T extends Base>
extends AsyncTask<Void, Void, TypedCursor<T>> {
private final Uri mContentUri;
private final String[] mProjection;
private final Class<T> mClazz;
private final ContentResolver mContentResolver;
private final SimpleSelectionFilter mFilter;
private final String mConstraint;
private final CursorLoader<T> mLoader;
private final CrudEventBus mBus;
public FetchTypedCursorAsyncTask(
Uri contentUri,
String[] projection,
Class<T> clazz,
ContentResolver contentResolver,
SimpleSelectionFilter<T> filter,
String constraint,
CursorLoader<T> loader,
CrudEventBus bus) {
mContentUri = contentUri;
mProjection = projection;
mClazz = clazz;
mContentResolver = contentResolver;
mFilter = filter;
mConstraint = constraint;
mLoader = loader;
mBus = bus;
}
@Override protected TypedCursor<T> doInBackground(Void... voids) {
Cursor cursor = null;
try {
cursor = mContentResolver.query(
mContentUri,
mProjection,
mFilter.getSelectionString(),
mFilter.getSelectionArgs(mConstraint),
null);
return new TypedCursorWithLoader<>(cursor, mLoader);
} catch (Exception e) {
if (cursor != null) {
cursor.close();
}
throw e;
}
}
@Override protected void onPostExecute(TypedCursor<T> result) {
mBus.post(TypedCursorFetchedEventFactory.createEvent(mClazz, result));
}
}
}