package com.jakehilborn.speedr; import android.content.Context; import android.util.Log; import android.widget.Toast; import com.crashlytics.android.Crashlytics; import com.crashlytics.android.answers.Answers; import com.crashlytics.android.answers.CustomEvent; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.jakehilborn.speedr.heremaps.HereMapsManager; import com.jakehilborn.speedr.heremaps.HereMapsService; import com.jakehilborn.speedr.heremaps.deserial.HereMapsResponse; import com.jakehilborn.speedr.heremaps.deserial.Response; import com.jakehilborn.speedr.overpass.OverpassInterceptor; import com.jakehilborn.speedr.overpass.OverpassManager; import com.jakehilborn.speedr.overpass.OverpassService; import com.jakehilborn.speedr.overpass.deserial.OverpassResponse; import com.jakehilborn.speedr.utils.ErrorReporter; import com.jakehilborn.speedr.utils.Prefs; import com.jakehilborn.speedr.utils.UnitUtils; import java.io.IOException; import java.lang.annotation.Annotation; import java.net.HttpURLConnection; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; import retrofit2.Converter; import retrofit2.Retrofit; import retrofit2.adapter.rxjava.HttpException; import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; import rx.SingleSubscriber; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; public class LimitFetcher { private static final String RADIUS = "25"; //meters private OverpassService overpassService; private Subscription overpassSubscription; private OverpassManager overpassManager; private HereMapsService hereMapsService; private Converter<ResponseBody, HereMapsResponse> hereMapsErrorConverter; private Subscription hereMapsSubscription; private HereMapsManager hereMapsManager; private Toast hereMapsError; public static final String USER_AGENT = "Speedr/" + BuildConfig.VERSION_NAME; public LimitFetcher(StatsCalculator statsCalculator) { buildOverpassService(); overpassManager = new OverpassManager(statsCalculator); buildHereMapsService(); hereMapsManager = new HereMapsManager(statsCalculator); } private void buildOverpassService() { Gson gson = new GsonBuilder().create(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .addInterceptor(new OverpassInterceptor()) .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://dummy.url/") //OverpassInterceptor will choose the appropriate Overpass endpoint .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build(); overpassService = retrofit.create(OverpassService.class); } private void buildHereMapsService() { Gson gson = new GsonBuilder().create(); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://route.api.here.com/routing/7.2/") .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build(); hereMapsService = retrofit.create(HereMapsService.class); hereMapsErrorConverter = retrofit.responseBodyConverter(HereMapsResponse.class, new Annotation[0]); } public void fetchLimit(Context context, Double lat, Double lon) { if (Prefs.isUseHereMaps(context)) { fetchHereMapsLimit(context, lat, lon); } else { fetchOverpassLimit(lat, lon); } } private void fetchOverpassLimit(final Double lat, final Double lon) { if (overpassSubscription != null) return; //Active request to Overpass has not responded yet String data = "[out:json];way(around:" + RADIUS + "," + lat + "," + lon + ")[\"highway\"][\"maxspeed\"];out;"; overpassSubscription = overpassService.getLimit(data) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new SingleSubscriber<OverpassResponse>() { @Override public void onSuccess(OverpassResponse overpassResponse) { overpassSubscription = null; overpassManager.handleResponse(overpassResponse, lat, lon); } @Override public void onError(Throwable error) { overpassSubscription = null; //Error was already logged in OverpassInterceptor } }); } private void fetchHereMapsLimit(final Context context, final Double lat, final Double lon) { if (hereMapsSubscription != null) return; //Active request to Here Maps has not responded yet final boolean isUseKph = Prefs.isUseKph(context); String appId = Prefs.getHereAppId(context); String appCode = Prefs.getHereAppCode(context); String waypoint = lat + "," + lon; hereMapsSubscription = hereMapsService.getLimit(appId, appCode, "roadName", waypoint, USER_AGENT) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new SingleSubscriber<HereMapsResponse>() { @Override public void onSuccess(HereMapsResponse hereMapsResponse) { hereMapsSubscription = null; Prefs.setPendingHereActivation(context, false); hereMapsManager.handleResponse(hereMapsResponse, lat, lon, isUseKph); } @Override public void onError(Throwable error) { hereMapsSubscription = null; int statusCode = -1; String type = ""; String subType = ""; String details = ""; if (error instanceof HttpException) { try { ResponseBody body = ((HttpException) error).response().errorBody(); Response hereResponse = hereMapsErrorConverter.convert(body).getResponse(); statusCode = ((HttpException) error).code(); type = hereResponse.getType(); subType = hereResponse.getSubtype(); details = hereResponse.getDetails(); } catch (IOException ioe) { Crashlytics.logException(ioe); } } else { ErrorReporter.logHereError(error); return; } //New HERE accounts take up to an hour to activate. New creds returns 403, invalid creds returns 401. //If new account, show notice on MainActivity instead of showing error as Toast. Then retry the request using Overpass. if (statusCode == HttpURLConnection.HTTP_FORBIDDEN && System.currentTimeMillis() < Prefs.getTimeOfHereCreds(context) + UnitUtils.secondsToMillis(60 * 60)) { Prefs.setPendingHereActivation(context, true); fetchOverpassLimit(lat, lon); Answers.getInstance().logCustom(new CustomEvent("Pending HERE Activation")); return; } //Show error message to user via Toast if (!type.isEmpty() || !subType.isEmpty() || !details.isEmpty()) { Prefs.setPendingHereActivation(context, false); String toastText = type + " - " + subType + "\n\n" + details; if (hereMapsError != null) hereMapsError.cancel(); //Cancel previous toast so they don't queue up hereMapsError = Toast.makeText(context, toastText, Toast.LENGTH_LONG); hereMapsError.show(); } ErrorReporter.logHereError(statusCode, type, subType, details); } }); } public void destroy(Context context) { Crashlytics.log(Log.INFO, LimitFetcher.class.getSimpleName(), "destroy()"); if (overpassSubscription != null) overpassSubscription.unsubscribe(); if (hereMapsSubscription != null) hereMapsSubscription.unsubscribe(); if (hereMapsError != null) hereMapsError.cancel(); Prefs.setPendingHereActivation(context, false); //Set to false so the next time MainService is started the pending activation notice does not show } }