/** * Copyright (C) 2013 - 2015 the enviroCar community * <p> * This file is part of the enviroCar app. * <p> * The enviroCar app is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * <p> * The enviroCar app 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 General * Public License for more details. * <p> * You should have received a copy of the GNU General Public License along * with the enviroCar app. If not, see http://www.gnu.org/licenses/. */ package org.envirocar.app.handler; import android.app.Activity; import android.content.Context; import com.google.common.base.Preconditions; import org.envirocar.app.R; import org.envirocar.app.exception.NotLoggedInException; import org.envirocar.app.exception.TrackAlreadyUploadedException; import org.envirocar.app.rxutils.ItemForwardSubscriber; import org.envirocar.app.rxutils.SingleItemForwardSubscriber; import org.envirocar.core.entity.Track; import org.envirocar.core.exception.NoMeasurementsException; import org.envirocar.core.exception.TrackWithNoValidCarException; import org.envirocar.core.injection.InjectApplicationScope; import org.envirocar.core.logging.Logger; import org.envirocar.core.utils.TrackUtils; import org.envirocar.remote.DAOProvider; import org.envirocar.storage.EnviroCarDB; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; import rx.Observable; import rx.Subscriber; import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; /** * Manager that can upload tracks and cars to the server. * Use the uploadAllTracks function to upload all local tracks. * Make sure that you specify the dbAdapter when instantiating. * The default constructor should only be used when there is no * other way. */ @Singleton public class TrackUploadHandler { private static Logger LOG = Logger.getLogger(TrackUploadHandler.class); private final Context mContext; private final EnviroCarDB mEnviroCarDB; private final CarPreferenceHandler mCarManager; private final DAOProvider mDAOProvider; private final TrackDAOHandler trackDAOHandler; private final UserHandler mUserManager; private final TermsOfUseManager mTermsOfUseManager; /** * Normal constructor for this manager. Specify the context and the dbadapter. * * @param context the context of the current scope */ @Inject public TrackUploadHandler( @InjectApplicationScope Context context, EnviroCarDB enviroCarDB, CarPreferenceHandler carPreferenceHandler, DAOProvider daoProvider, TrackDAOHandler trackDAOHandler, UserHandler userHandler, TermsOfUseManager termsOfUseManager) { this.mContext = context; this.mEnviroCarDB = enviroCarDB; this.mCarManager = carPreferenceHandler; this.mDAOProvider = daoProvider; this.trackDAOHandler = trackDAOHandler; this.mUserManager = userHandler; this.mTermsOfUseManager = termsOfUseManager; } /** * Returns an observable that uploads a single track. * * @param track the track to upload. * @param activity * @return an observable that uploads a single track. */ public Observable<Track> uploadTrackObservable(Track track, Activity activity) { return Observable.create(new Observable.OnSubscribe<Track>() { @Override public void call(Subscriber<? super Track> subscriber) { LOG.info("uploadTrackObservable() start uploading."); subscriber.onStart(); // Create a dialog with which the user can accept the terms of use. subscriber.add(Observable.just(track) // Verify whether the TermsOfUSe have been accepted. // When the TermsOfUse have not been accepted, create an // Dialog to accept and continue when the user has accepted. .compose(TermsOfUseManager.TermsOfUseValidator.create(mTermsOfUseManager, activity)) // Continue when the TermsOfUse has been accepted, otherwise // throw an error .flatMap(track1 -> uploadTrack(track1)) // Only forward the results to the real subscriber. .subscribe(SingleItemForwardSubscriber.create(subscriber))); } }); } /** * Returns an observable that uploads a list of tracks. If a track did not contain enough * measurements, i.e. the track obfuscation is throwing a {@link NoMeasurementsException}, * then it returns null to its subscriber. * * @param tracks the list of tracks to upload. * @param abortOnNoMeasurements if true, then it also closes the complete stream. Otherwise, * it returns null to its subscriber. * @return an observable that uploads a list of tracks. */ public Observable<Track> uploadTracksObservable( List<Track> tracks, boolean abortOnNoMeasurements) { return uploadTracksObservable(tracks, abortOnNoMeasurements, null); } /** * Returns an observable that uploads a list of tracks. If a track did not contain enough * measurements, i.e. the track obfuscation is throwing a {@link NoMeasurementsException}, * then it returns null to its subscriber. In case when the terms of use has not been * accepted for the specific user and the input parameter is not null, then it automatically * creates a dialog where the user can accept the terms of use. * * @param tracks the list of tracks to upload. * @param abortOnNoMeasurements if true, then it also closes the complete stream. Otherwise, * it returns null to its subscriber. * @param activity the activity of the current scope. When the activity is not * null, then it creates a dialog where it can be accepted. * @return an observable that uploads a list of tracks. */ public Observable<Track> uploadTracksObservable( List<Track> tracks, boolean abortOnNoMeasurements, Activity activity) { Preconditions.checkState(tracks != null && !tracks.isEmpty(), "Input tracks cannot be null or empty."); return Observable.just(tracks) .compose(TermsOfUseManager.TermsOfUseValidator.create(mTermsOfUseManager, activity)) .flatMap(tracks1 -> Observable.from(tracks1)) .concatMap(track -> uploadTrack(track) .first() .lift(getUploadTracksOperator(abortOnNoMeasurements))); } private Observable<Track> uploadTrack(Track track) { return Observable.just(track) // general validation of the track .map(validateRequirementsForUpload()) // assets the car of the track and, in case it is not uploaded, it uploads the // car and sets the remoteId .compose(validateCarOfTrack()) // Update the track metadata. .compose(updateTrackMetadata()) // obfuscate the track. .map(asObfuscatedTrackWhenChecked()) // Upload the track .flatMap(obfTrack -> mDAOProvider.getTrackDAO().createTrackObservable(obfTrack)) // Update the database entry .flatMap(uploadedTrack -> mEnviroCarDB.updateTrackObservable(uploadedTrack)); } private Func1<Track, Track> validateRequirementsForUpload() { return new Func1<Track, Track>() { @Override public Track call(Track track) { if (!track.isLocalTrack()) { String infoText = String.format(mContext.getString(R.string .trackviews_is_already_uploaded), track.getName()); LOG.warn(infoText); throw OnErrorThrowable.from(new TrackAlreadyUploadedException(infoText)); } else if (track.getCar() == null) { String infoText = "Track has no car set. Please delete this track."; LOG.warn(infoText); throw OnErrorThrowable.from(new TrackWithNoValidCarException(infoText)); } else if (!mUserManager.isLoggedIn()) { String infoText = mContext.getString(R.string.trackviews_not_logged_in); LOG.info(infoText); throw OnErrorThrowable.from(new NotLoggedInException(infoText)); } return track; } }; } private Func1<Track, Track> asObfuscatedTrackWhenChecked() { return track -> { LOG.info("asObfuscatedTrackWhenChecked()"); if (PreferencesHandler.isObfuscationEnabled(mContext)) { LOG.info(String.format("obfuscation is enabled. Obfuscating track with %s " + "measurements.", "" + track.getMeasurements().size())); try { return TrackUtils.getObfuscatedTrack(track); } catch (NoMeasurementsException e) { throw OnErrorThrowable.from(e); } } else { LOG.info("obfuscation is disabled."); return track; } }; } private Observable.Transformer<Track, Track> validateCarOfTrack() { return trackObservable -> trackObservable.flatMap( track -> mCarManager .assertTemporaryCar(track.getCar()) .map(car -> { track.setCar(car); return track; })); } private Observable.Transformer<Track, Track> updateTrackMetadata() { return trackObservable -> trackObservable.flatMap( track -> trackDAOHandler .updateTrackMetadataObservable(track) .map(trackMetadata -> track)); } private Observable.Operator<Track, Track> getUploadTracksOperator(boolean abortOnNoMeasurements) { return subscriber -> new ItemForwardSubscriber<Track>((Subscriber<Track>) subscriber) { @Override public void onError(Throwable e) { LOG.info("onError() Track has not enough measurements to upload."); if (!abortOnNoMeasurements && e.getCause() instanceof NoMeasurementsException) { subscriber.onNext(null); onCompleted(); } else { subscriber.onError(e); unsubscribe(); } } }; } }