// 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.ui.dialogs; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.view.View; import android.widget.AdapterView; import android.widget.GridView; import com.google.common.base.Optional; import org.projectbuendia.client.R; import org.projectbuendia.client.events.CrudEventBus; import org.projectbuendia.client.events.data.AppLocationTreeFetchedEvent; import org.projectbuendia.client.events.data.PatientUpdateFailedEvent; import org.projectbuendia.client.models.AppModel; import org.projectbuendia.client.models.Location; import org.projectbuendia.client.models.LocationTree; import org.projectbuendia.client.models.Zones; import org.projectbuendia.client.ui.BigToast; import org.projectbuendia.client.ui.lists.LocationListAdapter; import org.projectbuendia.client.utils.Logger; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; /** A dialog that allows users to assign or change a patient's location. */ public final class AssignLocationDialog implements DialogInterface.OnDismissListener, AdapterView.OnItemClickListener { private static final Logger LOG = Logger.create(); @Nullable private AlertDialog mDialog; @Nullable private GridView mGridView; @Nullable private LocationListAdapter mAdapter; private final Context mContext; private final AppModel mAppModel; private final String mLocale; private final Runnable mOnDismiss; private final CrudEventBus mEventBus; private final EventBusSubscriber mEventBusSubscriber = new EventBusSubscriber(); private final Optional<String> mCurrentLocationUuid; private final LocationSelectedCallback mLocationSelectedCallback; private ProgressDialog mProgressDialog; private LocationTree mLocationTree; private boolean mRegistered; // TODO: Consider making this an event bus event rather than a callback so that we don't // have to worry about Activity context leaks. public interface LocationSelectedCallback { /** * Called when then user selects a location that is not the currently selected one. * @return whether to immediately dismiss the dialog. If {@code false}, the dialog will * disable further button presses and display a progress spinner until * {@link AssignLocationDialog#dismiss} is called. */ boolean onLocationSelected(String locationUuid); } /** * Instantiates an {@link AssignLocationDialog}. * @param context the Activity or Application context * @param appModel the {@link AppModel} from which locations will be fetched * @param locale the current locale * @param onDismiss a {@link Runnable} run when the dialog is dismissed * @param eventBus a {@link CrudEventBus} where location modification events will be posted * @param currentLocationUuid an optional UUID for the user's current location, which will be * highlighted if specified * @param locationSelectedCallback a {@link Runnable} run when a location is selected */ public AssignLocationDialog( Context context, AppModel appModel, String locale, Runnable onDismiss, CrudEventBus eventBus, Optional<String> currentLocationUuid, LocationSelectedCallback locationSelectedCallback) { mContext = checkNotNull(context); mAppModel = checkNotNull(appModel); mLocale = checkNotNull(locale); this.mOnDismiss = checkNotNull(onDismiss); mEventBus = checkNotNull(eventBus); mCurrentLocationUuid = currentLocationUuid; mLocationSelectedCallback = checkNotNull(locationSelectedCallback); } /** Returns true iff the dialog is currently displayed. */ public boolean isShowing() { return mDialog != null && mDialog.isShowing(); } /** Displays the dialog. */ public void show() { // We have to do this backwards thing instead of just inflating the view directly into the // AlertDialog because calling findViewById() before show() causes a crash. See // http://stackoverflow.com/a/15572855/996592 for the gory details. View contents = View.inflate(mContext, R.layout.dialog_assign_location, null); mGridView = (GridView) contents.findViewById(R.id.location_selection_locations); startListeningForLocations(); mDialog = new AlertDialog.Builder(mContext) .setTitle(R.string.action_assign_location) .setView(contents) .setOnDismissListener(this) .create(); mDialog.show(); } private void startListeningForLocations() { mRegistered = true; mEventBus.register(mEventBusSubscriber); mAppModel.fetchLocationTree(mEventBus, mLocale); } /** * Notifies the dialog that updating the patient's location has failed. * @param reason the reason why the failure occurred, picked from errors in * {@link PatientUpdateFailedEvent} */ public void onPatientUpdateFailed(int reason) { mAdapter.setSelectedLocationUuid(mCurrentLocationUuid); int errorMessageResource; switch (reason) { case PatientUpdateFailedEvent.REASON_INTERRUPTED: errorMessageResource = R.string.patient_location_error_interrupted; break; case PatientUpdateFailedEvent.REASON_NETWORK: case PatientUpdateFailedEvent.REASON_SERVER: errorMessageResource = R.string.patient_location_error_network; break; case PatientUpdateFailedEvent.REASON_NO_SUCH_PATIENT: errorMessageResource = R.string.patient_location_error_no_such_patient; break; case PatientUpdateFailedEvent.REASON_CLIENT: default: errorMessageResource = R.string.patient_location_error_unknown; break; } BigToast.show(mContext, errorMessageResource); mProgressDialog.dismiss(); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { String locationUuid = mAdapter.getItem(position).uuid; mAdapter.setSelectedLocationUuid(Optional.fromNullable(locationUuid)); mProgressDialog = ProgressDialog.show(mContext, mContext.getResources().getString(R.string.title_updating_patient), mContext.getResources().getString(R.string.please_wait), true); if (isCurrentTent(locationUuid) || mLocationSelectedCallback.onLocationSelected(locationUuid)) { dismiss(); } } private boolean isCurrentTent(String newTentUuid) { return mCurrentLocationUuid.equals(mAdapter.getSelectedLocationUuid()); } /** Dismisses the dialog and releases all dialog resources. */ public void dismiss() { if (mLocationTree != null) { mLocationTree.close(); } mProgressDialog.dismiss(); mDialog.dismiss(); } // TODO: Consider adding the ability to re-enable buttons if a server request fails. @Override public void onDismiss(DialogInterface dialog) { if (mRegistered) { mEventBus.unregister(mEventBusSubscriber); } mOnDismiss.run(); } private void setTents(LocationTree locationTree) { if (mGridView != null) { List<Location> locations = new ArrayList<>( locationTree.getDescendantsAtDepth(LocationTree.ABSOLUTE_DEPTH_TENT)); Location triageZone = locationTree.findByUuid(Zones.TRIAGE_ZONE_UUID); locations.add(0, triageZone); Location dischargedZone = locationTree.findByUuid(Zones.DISCHARGED_ZONE_UUID); locations.add(dischargedZone); mAdapter = new LocationListAdapter( mContext, locations, locationTree, mCurrentLocationUuid); mGridView.setAdapter(mAdapter); mGridView.setOnItemClickListener(this); mGridView.setSelection(1); } } private final class EventBusSubscriber { public void onEventMainThread(AppLocationTreeFetchedEvent event) { if (event.tree.getRoot() == null) { LOG.d("LocationTree has a null root, suggesting something went wrong."); return; } mLocationTree = event.tree; setTents(event.tree); } } }