package com.thebluealliance.androidclient.subscribers; import com.google.android.gms.analytics.Tracker; import com.thebluealliance.androidclient.TbaLogger; import com.thebluealliance.androidclient.datafeed.APISubscriber; import com.thebluealliance.androidclient.datafeed.DataConsumer; import com.thebluealliance.androidclient.datafeed.refresh.RefreshController; import com.thebluealliance.androidclient.helpers.AnalyticsHelper; import org.greenrobot.eventbus.EventBus; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import rx.Observer; import rx.android.schedulers.AndroidSchedulers; /** * Base class for a concrete API Subscriber. * This class takes an input of data directly from Retrofit (using {@link rx.Subscriber} and * provides a callback to * * Subclasses should implement {@link #parseData()} to take the data set in {@link #mAPIData} and * produce something bindable and store it in {@link #mDataToBind}. Subclasses should also * initialize {@link #mDataToBind} in their constructors, as it's possible that the variable * is accessed before {@link #parseData()} is called * * @param <APIType> Datatype to be returned from the API (one from * {@link com.thebluealliance.androidclient.api.rx.TbaApiV2} * @param <BindType> Datatype to be returned for binding to views */ public abstract class BaseAPISubscriber<APIType, BindType> implements Observer<APIType>, APISubscriber<BindType> { DataConsumer<BindType> mConsumer; protected APIType mAPIData; protected BindType mDataToBind; @Nullable RefreshController mRefreshController; //TODO hook up to DI String mRefreshTag; Tracker mAnalyticsTracker; boolean hasBinderBoundViews; boolean shouldBindImmediately; boolean shouldBindOnce; private long mRefreshStart; public BaseAPISubscriber() { shouldBindImmediately = true; hasBinderBoundViews = false; } public void setShouldBindImmediately(boolean shouldBind) { shouldBindImmediately = shouldBind; } /** * If set, bind immediately once until onComplete is called */ public void setShouldBindOnce(boolean shouldBind) { shouldBindOnce = shouldBind; } public void setConsumer(DataConsumer<BindType> consumer) { mConsumer = consumer; } public void setRefreshController(@Nullable RefreshController refreshController) { mRefreshController = refreshController; } public void setRefreshTag(String refreshTag) { mRefreshTag = refreshTag; } public void setTracker(Tracker tracker) { mAnalyticsTracker = tracker; } /** * Called when a refresh begins */ public void onRefreshStart(@RefreshController.RefreshType int refreshType) { if (mRefreshController != null) { mRefreshController.notifyRefreshingStateChanged(mRefreshTag, true); } mRefreshStart = System.nanoTime(); if (refreshType == RefreshController.REQUESTED_BY_USER) { sendRefreshUpdate(); } } @UiThread public void onParentStop() { hasBinderBoundViews = false; } @Override @WorkerThread public void onNext(APIType data) { setApiData(data); postToEventBus(EventBus.getDefault()); if (isDataValid()) { parseData(); } if (shouldBindImmediately || shouldBindOnce) { // bindViewsIfNeeded(); bindData(); } } @Override public void onCompleted() { shouldBindOnce = false; AndroidSchedulers.mainThread().createWorker().schedule(() -> { if (mRefreshController != null) { mRefreshController.notifyRefreshingStateChanged(mRefreshTag, false); } if (mConsumer != null) { try { bindViewsIfNeeded(); mConsumer.onComplete(); } catch (Exception e) { TbaLogger.e("UNABLE TO COMPLETE RENDER"); e.printStackTrace(); mConsumer.onError(e); } } }); long totalRefreshTime = System.nanoTime() - mRefreshStart; sendTimingUpdate(totalRefreshTime / 1000); // Convert to ms } @Override public void onError(Throwable throwable) { AndroidSchedulers.mainThread().createWorker().schedule(() -> { if (mRefreshController != null) { mRefreshController.notifyRefreshingStateChanged(mRefreshTag, false); } if (mConsumer != null) { bindViewsIfNeeded(); mConsumer.onError(throwable); } }); sendExceptionUpdate(throwable); } @Override public @Nullable BindType getBoundData() { return mDataToBind; } @VisibleForTesting public void setApiData(APIType data) { mAPIData = data; } public @Nullable APIType getApiData() { return mAPIData; } public void bindData() { AndroidSchedulers.mainThread().createWorker().schedule(() -> { if (mConsumer != null) { try { bindViewsIfNeeded(); mConsumer.updateData(mDataToBind); } catch (Exception e) { TbaLogger.e("UNABLE TO RENDER"); e.printStackTrace(); mConsumer.onError(e); } } }); } /** * */ public void bindViewsIfNeeded() { if (!hasBinderBoundViews && mConsumer != null) { mConsumer.bindViews(); hasBinderBoundViews = true; } } /** * Post {@link #mAPIData} to the given {@link EventBus} */ protected void postToEventBus(EventBus eventBus) { if (shouldPostToEventBus()) { eventBus.post(mAPIData); } } /** * Do we post {@link #mAPIData} to the EventBus? * Override and return true to do so */ protected boolean shouldPostToEventBus() { return false; } /** * Subclasses can override this method to determine if {@link #mAPIData} is valid. * Default to simply checking if null */ public boolean isDataValid() { return mAPIData != null; } private void sendTimingUpdate(long timeSpent) { if (mAnalyticsTracker != null) { mAnalyticsTracker.send(AnalyticsHelper.getTimingHit(timeSpent, this.getClass().getSimpleName(), mRefreshTag)); } } private void sendRefreshUpdate() { if (mAnalyticsTracker != null) { mAnalyticsTracker.send(AnalyticsHelper.getRefreshHit(mRefreshTag)); } } private void sendExceptionUpdate(Throwable throwable) { if (mAnalyticsTracker != null) { mAnalyticsTracker.send(AnalyticsHelper.getErrorHit(throwable)); } } }