/* * The CroudTrip! application aims at revolutionizing the car-ride-sharing market with its easy, * user-friendly and highly automated way of organizing shared Trips. Copyright (C) 2015 Nazeeh Ammari, * Philipp Eichhorn, Ricarda Hohn, Vanessa Lange, Alexander Popp, Frederik Simon, Michael Weber * This program is free software: you can redistribute it and/or modify it under the terms of the GNU * Affero General Public License as published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package org.croudtrip.fragments.offer; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.location.Location; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.NfcAdapter; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewManager; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.PolylineOptions; import com.pnikosis.materialishprogress.ProgressWheel; import org.croudtrip.Constants; import org.croudtrip.R; import org.croudtrip.api.TripsResource; import org.croudtrip.api.directions.NavigationResult; import org.croudtrip.api.directions.RouteLocation; import org.croudtrip.api.trips.JoinTripRequest; import org.croudtrip.api.trips.JoinTripRequestUpdate; import org.croudtrip.api.trips.JoinTripRequestUpdateType; import org.croudtrip.api.trips.JoinTripStatus; import org.croudtrip.api.trips.TripOffer; import org.croudtrip.api.trips.TripOfferDescription; import org.croudtrip.api.trips.TripOfferUpdate; import org.croudtrip.api.trips.UserWayPoint; import org.croudtrip.fragments.SubscriptionFragment; import org.croudtrip.location.LocationUpdater; import org.croudtrip.trip.MyTripDriverPassengersAdapter; import org.croudtrip.trip.OnDiversionUpdateListener; import org.croudtrip.utils.CrashCallback; import org.croudtrip.utils.DefaultTransformer; import org.croudtrip.utils.SwipeListener; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import javax.inject.Inject; import it.neokree.materialnavigationdrawer.MaterialNavigationDrawer; import it.neokree.materialnavigationdrawer.elements.MaterialSection; import roboguice.inject.InjectView; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action1; import rx.functions.Func1; import rx.schedulers.Schedulers; import timber.log.Timber; /** * This class shows a screen for the driver after he has offered a trip and hence is currently * already on his way. He is shown a map, his earnings and the passengers that he has accepted. * * @author Frederik Simon, Vanessa Lange */ public class MyTripDriverFragment extends SubscriptionFragment { //************************* Variables ****************************// // Route/Navigation public static final String ARG_ACTION = "ARG_ACTION"; public static final String ACTION_LOAD = "ACTION_LOAD"; public static final String ACTION_CREATE = "ACTION_CREATE"; private GoogleMap googleMap; private NfcAdapter nfcAdapter; private NdefMessage ndefMessage; private long offerID = -1; // Passengers list private MyTripDriverPassengersAdapter adapter; private SwipeListener touchListener; @InjectView(R.id.rv_my_trip_driver_passengers) private RecyclerView recyclerView; private ProgressWheel mapProgressBar; private ProgressWheel passengersProgressBar; private ProgressWheel finishProgressBar; private ProgressWheel cancelProgressBar; private ProgressWheel generalProgressBar; @InjectView(R.id.iv_transparent_image) private ImageView transparentImageView; @Inject private TripsResource tripsResource; @Inject private LocationUpdater locationUpdater; private Button finishButton; private Button cancelButton; // Detect if a passenger cancels his trip or has reached his destination private BroadcastReceiver passengersChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // Load the whole offer incl. passengers etc. again loadOffer(); } }; //************************* Methods ****************************// @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); setHasOptionsMenu(true); nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); NdefRecord ndefRecord = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], null); ndefMessage = new NdefMessage(new NdefRecord[]{ndefRecord}); return inflater.inflate(R.layout.fragment_my_trip_driver, container, false); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Fill the passengers list View header = view.findViewById(R.id.ll_my_trip_driver_info); adapter = new MyTripDriverPassengersAdapter(this, header); AcceptDeclineRequestListener acceptDeclineListener = new AcceptDeclineRequestListener(); this.touchListener = new SwipeListener(recyclerView, acceptDeclineListener); adapter.setOnRequestAcceptDeclineListener(acceptDeclineListener); mapProgressBar = (ProgressWheel) adapter.getHeader().findViewById(R.id.pb_my_trip_map_progressBar); passengersProgressBar = (ProgressWheel) adapter.getHeader().findViewById(R.id.pb_my_trip_passengers_progressBar); finishProgressBar = (ProgressWheel) adapter.getHeader().findViewById(R.id.pb_my_trip_finish); cancelProgressBar = (ProgressWheel) adapter.getHeader().findViewById(R.id.pb_my_trip_cancel); generalProgressBar = (ProgressWheel) view.findViewById(R.id.pb_my_trip_progressBar); RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity()); recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(adapter); recyclerView.setOnTouchListener(touchListener); recyclerView.setOnScrollListener(touchListener.makeScrollListener()); setupCancelButton(header); setupFinishButton(header); // Get the route to display it on the map SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager() .findFragmentById(R.id.f_my_trip_driver_map); // TODO: Make it asynchronously googleMap = mapFragment.getMap(); Bundle bundle = getArguments(); if (bundle == null) { bundle = new Bundle(); } String action = bundle.getString(ARG_ACTION, ACTION_LOAD); if (action.equals(ACTION_CREATE)) { Timber.d("Create Offer"); createOffer(bundle); } else { Timber.d("Load Offer"); loadOffer(); } transparentImageView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: recyclerView.requestDisallowInterceptTouchEvent(true); return false; case MotionEvent.ACTION_UP: recyclerView.requestDisallowInterceptTouchEvent(false); return true; case MotionEvent.ACTION_MOVE: recyclerView.requestDisallowInterceptTouchEvent(true); return false; default: return true; } } }); // Remove the header from the layout. Otherwise it exists twice ((ViewManager) view).removeView(header); } @Override public void onResume() { super.onResume(); if (nfcAdapter != null) { nfcAdapter.setNdefPushMessage(ndefMessage, getActivity()); } // Get notified if a passenger cancels his trip or has reached his destination IntentFilter filter = new IntentFilter(); filter.addAction(Constants.EVENT_PASSENGER_CANCELLED_TRIP); filter.addAction(Constants.EVENT_PASSENGER_REACHED_DESTINATION); filter.addAction(Constants.EVENT_PASSENGER_ENTERED_CAR); filter.addAction(Constants.EVENT_NEW_JOIN_REQUEST); LocalBroadcastManager.getInstance(getActivity().getApplicationContext()) .registerReceiver(passengersChangeReceiver, filter); } @Override public void onPause() { super.onPause(); LocalBroadcastManager.getInstance(getActivity().getApplicationContext()) .unregisterReceiver(passengersChangeReceiver); } private void setupCancelButton(View header) { cancelButton = ((Button) (header.findViewById(R.id.btn_my_trip_driver_cancel_trip))); cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { cancelButton.setEnabled(false); cancelProgressBar.setVisibility(View.VISIBLE); // Tell the server to cancel this trip subscriptions.add( tripsResource.updateOffer(offerID, TripOfferUpdate.createCancelUpdate()) .compose(new DefaultTransformer<TripOffer>()) .subscribe(new Action1<TripOffer>() { @Override public void call(TripOffer offer) { // After the server has been contacted successfully, clean // up the SharedPref and show "Offer Trip" screen again removeRunningTripOfferState(); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { Toast.makeText(getActivity(), R.string.join_trip_results_error, Toast.LENGTH_SHORT).show(); cancelButton.setEnabled(true); cancelProgressBar.setVisibility(View.GONE); } })); } }); } private void setupFinishButton(View header) { finishButton = ((Button) (header.findViewById(R.id.btn_my_trip_driver_finish_trip))); finishButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finishButton.setEnabled(false); finishProgressBar.setVisibility(View.VISIBLE); // Tell the server to finish this trip subscriptions.add( tripsResource.updateOffer(offerID, TripOfferUpdate.createFinishUpdate()) .compose(new DefaultTransformer<TripOffer>()) .subscribe(new Action1<TripOffer>() { @Override public void call(TripOffer offer) { // After the server has been contacted successfully, clean // up the SharedPref and show "Offer Trip" screen again removeRunningTripOfferState(); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { Toast.makeText(getActivity(), R.string.join_trip_results_error, Toast.LENGTH_SHORT).show(); finishButton.setEnabled(true); finishProgressBar.setVisibility(View.GONE); } })); } }); } /** * Downloads the current offer and processes its information with the {@link LoadOfferSubscriber} */ private synchronized void loadOffer() { // UI mapProgressBar.setVisibility(View.VISIBLE); // Don't show spinner again, looks ugly with already filled adapter // passengersProgressBar.setVisibility(View.VISIBLE); subscriptions.add(tripsResource.getActiveOffers() .compose(new DefaultTransformer<List<TripOffer>>()) .flatMap(new Func1<List<TripOffer>, Observable<TripOffer>>() { @Override public Observable<TripOffer> call(List<TripOffer> tripOffers) { if (tripOffers.isEmpty()) { // Inform user String errorMsg = getString(R.string.navigation_error_no_offer); Toast.makeText(getActivity(), errorMsg, Toast.LENGTH_LONG).show(); removeRunningTripOfferState(); throw new NoSuchElementException(errorMsg); } return Observable.just(tripOffers.get(0)); } }).subscribe(new LoadOfferSubscriber())); } private void loadPassengers() { subscriptions.add(tripsResource.getJoinRequests(false) .compose(new DefaultTransformer<List<JoinTripRequest>>()) .subscribe(new ImportantPassengersSubscriber()) ); } public void informAboutDiversion(final JoinTripRequest joinRequest, final OnDiversionUpdateListener listener, final TextView textView) { // Ask the server for the diversion subscriptions.add(tripsResource .getDiversionInSecondsForJoinRequest(joinRequest.getId()) .compose(new DefaultTransformer<Long>()) .subscribe(new Action1<Long>() { @Override public void call(Long diversionInSeconds) { int diversionInMinutes = (int) (diversionInSeconds / 60); listener.onDiversionUpdate(joinRequest, textView, diversionInMinutes); } }, new CrashCallback(getActivity(), "failed to get diversion"))); } private void removeRunningTripOfferState() { SharedPreferences prefs = getActivity().getSharedPreferences(Constants.SHARED_PREF_FILE_PREFERENCES, Context.MODE_PRIVATE); prefs.edit().remove(Constants.SHARED_PREF_KEY_RUNNING_TRIP_OFFER).apply(); // Change "My Trip" (driver) to "Offer Trip" in navigation drawer MaterialNavigationDrawer drawer = ((MaterialNavigationDrawer) getActivity()); // Find "last" "My Trip", so we don't accidentally rename the join-trip-my trip List<MaterialSection> sections = drawer.getSectionList(); MaterialSection section = null; for (MaterialSection s : sections) { if (s.getTitle().equals(getString(R.string.menu_my_trip))) { section = s; } } section.setTitle(getString(R.string.menu_offer_trip)); // The next fragment shows the "Offer trip" screen drawer.setFragment(new OfferTripFragment(), getString(R.string.menu_offer_trip)); } private void generateRouteOnMap(TripOffer offer, NavigationResult navigationResult) { // only one route will be shown (old route will be deleted googleMap.clear(); // get the polyline that should be shown as a list of RouteLocation objects and convert // it to LatLng objects. We have to do it this way, because the models can not import // the android maps libraries since the models also exist on the server. List<RouteLocation> polyline = navigationResult.getRoute().getPolylineWaypointsForUser(offer.getDriver(), navigationResult.getUserWayPoints()); List<LatLng> polylinePoints = new ArrayList<LatLng>(); for (RouteLocation loc : polyline) polylinePoints.add(new LatLng(loc.getLat(), loc.getLng())); //Timber.d("polyline: " + polyline); // Show route information on the map googleMap.addPolyline(new PolylineOptions().addAll(polylinePoints)); googleMap.setMyLocationEnabled(true); for (UserWayPoint userWp : navigationResult.getUserWayPoints()) { if (!userWp.getUser().equals(offer.getDriver())) { googleMap.addMarker( new MarkerOptions() .position(new LatLng(userWp.getLocation().getLat(), userWp.getLocation().getLng())) .icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_marker)) .anchor(0.5f, 0.5f) .flat(true) ); } } // Move camera to current position Location location = locationUpdater.getLastLocation(); if (location == null) return; LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 10); googleMap.animateCamera(cameraUpdate); mapProgressBar.setVisibility(View.GONE); } private void createOffer(Bundle arguments) { int maxDiversion = arguments.getInt("maxDiversion"); int pricePerKilometer = arguments.getInt("pricePerKilometer"); double fromLat = arguments.getDouble("fromLat"); double fromLng = arguments.getDouble("fromLng"); double toLat = arguments.getDouble("toLat"); double toLng = arguments.getDouble("toLng"); long vehicleId = arguments.getLong("vehicle_id"); TripOfferDescription tripOffer = new TripOfferDescription( new RouteLocation(fromLat, fromLng), new RouteLocation(toLat, toLng), maxDiversion * 1000L, pricePerKilometer, vehicleId); tripsResource.addOffer(tripOffer) .compose(new DefaultTransformer<TripOffer>()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<TripOffer>() { @Override public void call(TripOffer tripOffer) { // SUCCESS Timber.d("Your offer was successfully sent to the server"); offerID = tripOffer.getId(); // show route information on the map generateRouteOnMap(tripOffer, NavigationResult.createNavigationResultForDriverRoute(tripOffer)); loadPassengers(); // Remember that a trip was offered to show "My Trip" instead of "Offer Trip" // in the Navigation drawer getActivity().getSharedPreferences(Constants.SHARED_PREF_FILE_PREFERENCES, Context.MODE_PRIVATE) .edit().putBoolean(Constants.SHARED_PREF_KEY_RUNNING_TRIP_OFFER, true).apply(); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { // ERROR Timber.e(throwable.getMessage()); // Inform user Toast.makeText(getActivity(), getString(R.string.offer_trip_failed), Toast.LENGTH_LONG).show(); removeRunningTripOfferState(); } }); } //*************************** Inner classes ********************************// /** * This Subscriber loads the current route on the map and load the passengers with the help of * {@link ImportantPassengersSubscriber} */ private class LoadOfferSubscriber extends Subscriber<TripOffer> { @Override public void onNext(final TripOffer offer) { if (offer == null) { throw new NoSuchElementException(getString(R.string.navigation_error_no_offer)); } Timber.d("Received Offer with ID " + offer.getId()); // Remember the offerID for later and load the passengers for this offer offerID = offer.getId(); loadPassengers(); // Load the Route tripsResource.computeNavigationResultForOffer(offerID) .compose(new DefaultTransformer<NavigationResult>()) .subscribe(new Action1<NavigationResult>() { @Override public void call(NavigationResult navigationResult) { if (navigationResult == null) { throw new NoSuchElementException("No route available"); } generateRouteOnMap(offer, navigationResult); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { onError(throwable); } } ); } @Override public void onCompleted() { } @Override public void onError(Throwable throwable) { mapProgressBar.setVisibility(View.GONE); passengersProgressBar.setVisibility(View.GONE); Timber.e(throwable.getMessage()); Toast.makeText(getActivity(), getString(R.string.load_trip_failed), Toast.LENGTH_LONG).show(); } } /** * This Subscriber checks if there are still "important" passengers to take care of. That is, * e.g. there are still passengers sitting in the car or there are still passengers accepted. * If there are none, the driver may click the finishButton. Furthermore, all * relevant passengers (all that were not declined or canceled), will be shown in the RecyclerView-list. */ private class ImportantPassengersSubscriber extends Subscriber<List<JoinTripRequest>> { @Override public synchronized void onNext(List<JoinTripRequest> joinTripRequests) { // Only allow finish if there are no passengers in the car or accepted boolean allowFinish = true; boolean allowCancel = true; for (JoinTripRequest joinTripRequest : joinTripRequests) { // TODO: Filter already on server if (joinTripRequest.getOffer().getId() != offerID) { continue; } JoinTripStatus status = joinTripRequest.getStatus(); if (status == JoinTripStatus.PASSENGER_ACCEPTED) { adapter.updatePendingPassenger(joinTripRequest); } else { // Passenger should not appear in pending requests list adapter.removePendingPassenger(joinTripRequest.getId()); } if (status != JoinTripStatus.DRIVER_DECLINED && status != JoinTripStatus.PASSENGER_ACCEPTED) { if (status == JoinTripStatus.PASSENGER_CANCELLED || status == JoinTripStatus.DRIVER_CANCELLED) { // Remove any cancelled passengers from the adapter adapter.removePassenger(joinTripRequest.getId()); } else if (status == JoinTripStatus.DRIVER_ACCEPTED || status == JoinTripStatus.PASSENGER_IN_CAR || status == JoinTripStatus.PASSENGER_AT_DESTINATION) { // Simply update (or implicitly add) this passenger request adapter.updatePassenger(joinTripRequest); if (status == JoinTripStatus.DRIVER_ACCEPTED || status == JoinTripStatus.PASSENGER_IN_CAR) { Timber.d("Finishing trip not allowed: there is still an important passenger: " + status); allowFinish = false; if (status == JoinTripStatus.PASSENGER_IN_CAR) { allowCancel = false; Timber.d("Cancelling trip not allowed: there is still a passenger in the car"); } } } } } // Dis-/Allow the driver to finish the trip finishButton.setEnabled(allowFinish); cancelButton.setEnabled(allowCancel); adapter.maintainOrder(); adapter.updateEarnings(); // notifyDataSetChanged is called automatically } @Override public void onCompleted() { passengersProgressBar.setVisibility(View.GONE); } @Override public void onError(Throwable e) { passengersProgressBar.setVisibility(View.GONE); Timber.e("Receiving Passengers (JoinTripRequest) failed:\n" + e.getMessage()); Toast.makeText(getActivity(), getString(R.string.load_passengers_failed), Toast.LENGTH_LONG).show(); } } private class AcceptDeclineRequestListener implements SwipeListener.DismissCallbacks, MyTripDriverPassengersAdapter.OnRequestAcceptDeclineListener { /** * Listener to listen for any driver decisions to accept or decline * a pending JoinTripRequest. As soon as such a decision is received, the server * is contacted. * * @param accept if this method should handle "accept" (true) or "decline" (false) * @param position the position of the clicked JoinTripRequest in the adapter */ private synchronized void handleAcceptDecline(final boolean accept, final int position) { Timber.d("Swiped position: " + position); String task; if (accept) { task = "Accepting"; } else { task = "Declining"; } final JoinTripRequest request = adapter.getPendingPassenger(position - 1); // -1 because of header Timber.i(task + " Request with ID " + request.getId()); // UI generalProgressBar.setVisibility(View.VISIBLE); // Don't allow other user clicks while the task is performed recyclerView.setOnTouchListener(null); //Get a list of current Join trip requests from the server //and make sure that the request is still active (hasn't expired) subscriptions.add(tripsResource.getJoinRequests(true) .compose(new DefaultTransformer<List<JoinTripRequest>>()) .subscribe(new Action1<List<JoinTripRequest>>() { @Override public void call(List<JoinTripRequest> requests) { if (requests.size() > 0) { for (int i = 0; i < requests.size(); i++) { if (request.getId() == requests.get(i).getId()) { //Inform server only if the request is still active (not expired) JoinTripRequestUpdate requestUpdate; if (accept) { requestUpdate = new JoinTripRequestUpdate(JoinTripRequestUpdateType.ACCEPT_PASSENGER); } else { requestUpdate = new JoinTripRequestUpdate(JoinTripRequestUpdateType.DECLINE_PASSENGER); } subscriptions.add(tripsResource.updateJoinRequest(request.getId(), requestUpdate) .compose(new DefaultTransformer<JoinTripRequest>()) .subscribe(new AcceptDeclineRequestSubscriber(accept))); Timber.i("Request has not expired"); break; } //If the request wasn't found in the list, show a toast to the driver // and remove the card from the list if (i == requests.size() - 1) { Timber.d("Request has expired"); Toast.makeText(getActivity(), getResources().getString(R.string.offer_trip_request_expired), Toast.LENGTH_SHORT).show(); //Enable clicking the list items again and remove the progress bar recyclerView.setOnTouchListener(touchListener); generalProgressBar.setVisibility(View.GONE); adapter.removePendingPassenger(request.getId()); } } } else { //If the expired request was the last one in the list, size() will be 0 //This snippet takes care of this case Timber.d("No requests found (Request has expired)"); Toast.makeText(getActivity(), getResources().getString(R.string.offer_trip_request_expired), Toast.LENGTH_SHORT).show(); //Enable clicking the list items again and remove the progress bar recyclerView.setOnTouchListener(touchListener); generalProgressBar.setVisibility(View.GONE); adapter.removePendingPassenger(request.getId()); } adapter.maintainOrder(); adapter.notifyDataSetChanged(); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { Toast.makeText(getActivity(), R.string.join_trip_results_error, Toast.LENGTH_SHORT).show(); generalProgressBar.setVisibility(View.GONE); recyclerView.setOnTouchListener(touchListener); } })); } @Override public void onJoinRequestDecline(View view, int position) { handleAcceptDecline(false, position); } @Override public void onJoinRequestAccept(View view, int position) { handleAcceptDecline(true, position); } @Override public boolean canDismiss(int position) { return adapter.isPositionPendingPassenger(position); } @Override public void onSwipeLeft(RecyclerView recyclerView, int[] dismissedItems) { // Decline only the first item and ignore the rest if (dismissedItems != null && dismissedItems.length > 0) { handleAcceptDecline(false, dismissedItems[0]); } } @Override public void onSwipeRight(RecyclerView recyclerView, int[] dismissedItems) { // Accept only the first item and ignore the rest if (dismissedItems != null && dismissedItems.length > 0) { handleAcceptDecline(true, dismissedItems[0]); } } } /** * A simple Subscriber that removes the previously accepted/declined request * from the adapter */ private class AcceptDeclineRequestSubscriber extends Subscriber<JoinTripRequest> { private boolean accept; protected AcceptDeclineRequestSubscriber(boolean accept) { super(); this.accept = accept; } @Override public void onNext(JoinTripRequest joinTripRequest) { // SUCCESS Timber.i("Successfully informed the server about a request with ID " + joinTripRequest.getId()); // Everything worked out, so remove the request from the adapter adapter.removePendingPassenger(joinTripRequest.getId()); adapter.notifyDataSetChanged(); if (accept) { loadOffer(); } } @Override public void onError(Throwable e) { // ERROR String task; if (accept) { task = "accepting"; } else { task = "declining"; } Timber.e("Error when " + task + " a JoinTripRequest: " + e.getMessage()); onDone(); } @Override public void onCompleted() { onDone(); } private void onDone() { // Allow clicks on trips again //adapter.setOnRequestAcceptDeclineListener(new AcceptDeclineRequestListener()); recyclerView.setOnTouchListener(touchListener); // UI generalProgressBar.setVisibility(View.GONE); } } }