public class AnimationFactory {

    /**
     * The {@code FlipDirection} enumeration defines the most typical flip view transitions: left-to-right and right-to-left. {@code FlipDirection} is used during the creation of {@link FlipAnimation} animations.
     *
     * @author Ephraim A. Tekle
     */
    public enum FlipDirection {
        LEFT_RIGHT,
        RIGHT_LEFT;

        public float getStartDegreeForFirstView() {
            return 0;
        }

        public float getStartDegreeForSecondView() {
            switch (this) {
                case LEFT_RIGHT:
                    return -90;
                case RIGHT_LEFT:
                    return 90;
                default:
                    return 0;
            }
        }

        public float getEndDegreeForFirstView() {
            switch (this) {
                case LEFT_RIGHT:
                    return 90;
                case RIGHT_LEFT:
                    return -90;
                default:
                    return 0;
            }
        }

        public float getEndDegreeForSecondView() {
            return 0;
        }

        public FlipDirection theOtherDirection() {
            switch (this) {
                case LEFT_RIGHT:
                    return RIGHT_LEFT;
                case RIGHT_LEFT:
                    return LEFT_RIGHT;
                default:
                    return null;
            }
        }
    }

    /**
     * Create a pair of {@link FlipAnimation} that can be used to flip 3D transition from {@code fromView} to {@code toView}. A typical use case is with {@link ViewAnimator} as an out and in transition.
     * <p/>
     * NOTE: Avoid using this method. Instead, use {@link #flipTransition}.
     *
     * @param fromView     the view transition away from
     * @param toView       the view transition to
     * @param dir          the flip direction
     * @param duration     the transition duration in milliseconds
     * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator)
     * @return animation pair to be used for IN and OUT animations
     */
    public static Animation[] flipAnimation(final View fromView, final View toView, FlipDirection dir, long duration, Interpolator interpolator) {
        Animation[] result = new Animation[2];
        float centerX;
        float centerY;

        centerX = fromView.getWidth() / 2.0f;
        centerY = fromView.getHeight() / 2.0f;

        Animation outFlip = new FlipAnimation(dir.getStartDegreeForFirstView(), dir.getEndDegreeForFirstView(), centerX, centerY, FlipAnimation.SCALE_DEFAULT, FlipAnimation.ScaleUpDownEnum.SCALE_DOWN);
        outFlip.setDuration(duration);
        outFlip.setFillAfter(true);
        outFlip.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);

        AnimationSet outAnimation = new AnimationSet(true);
        outAnimation.addAnimation(outFlip);
        result[0] = outAnimation;

        // Uncomment the following if toView has its layout established (not the case if using ViewFlipper and on first show)
        //centerX = toView.getWidth() / 2.0f;
        //centerY = toView.getHeight() / 2.0f;

        Animation inFlip = new FlipAnimation(dir.getStartDegreeForSecondView(), dir.getEndDegreeForSecondView(), centerX, centerY, FlipAnimation.SCALE_DEFAULT, FlipAnimation.ScaleUpDownEnum.SCALE_UP);
        inFlip.setDuration(duration);
        inFlip.setFillAfter(true);
        inFlip.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);
        inFlip.setStartOffset(duration);

        AnimationSet inAnimation = new AnimationSet(true);
        inAnimation.addAnimation(inFlip);
        result[1] = inAnimation;

        return result;

    }


    /**
     * Create a pair of {@link FlipAnimation} that can be used to flip 3D transition from {@code fromView} to {@code toView}. A typical use case is with {@link ViewAnimator} as an out and in transition.
     * <p/>
     * NOTE: Avoid using this method. Instead, use {@link #flipTransition}.
     *
     * @param width       the view transition to
     * @param dir          the flip direction
     * @param duration     the transition duration in milliseconds
     * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator)
     * @return animation pair to be used for IN and OUT animations
     */
    public static Animation[] flipAnimation(final int width, FlipDirection dir, long duration, Interpolator interpolator) {
        Animation[] result = new Animation[2];
        float centerX;
        float centerY;

        centerX = width / 2.0f;
        centerY = width / 2.0f;

        Animation outFlip = new FlipAnimation(dir.getStartDegreeForFirstView(), dir.getEndDegreeForFirstView(), centerX, centerY, FlipAnimation.SCALE_DEFAULT, FlipAnimation.ScaleUpDownEnum.SCALE_DOWN);
        outFlip.setDuration(duration);
        outFlip.setFillAfter(true);
        outFlip.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);

        AnimationSet outAnimation = new AnimationSet(true);
        outAnimation.addAnimation(outFlip);
        result[0] = outAnimation;

        // Uncomment the following if toView has its layout established (not the case if using ViewFlipper and on first show)
        //centerX = toView.getWidth() / 2.0f;
        //centerY = toView.getHeight() / 2.0f;

        Animation inFlip = new FlipAnimation(dir.getStartDegreeForSecondView(), dir.getEndDegreeForSecondView(), centerX, centerY, FlipAnimation.SCALE_DEFAULT, FlipAnimation.ScaleUpDownEnum.SCALE_UP);
        inFlip.setDuration(duration);
        inFlip.setFillAfter(true);
        inFlip.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);
        inFlip.setStartOffset(duration);

        AnimationSet inAnimation = new AnimationSet(true);
        inAnimation.addAnimation(inFlip);
        result[1] = inAnimation;

        return result;

    }

    /**
     * Flip to the next view of the {@code ViewAnimator}'s subviews. A call to this method will initiate a {@link FlipAnimation} to show the next View.
     * If the currently visible view is the last view, flip direction will be reversed for this transition.
     *
     * @param viewAnimator the {@code ViewAnimator}
     * @param dir          the direction of flip
     */
    public static void flipTransition(final ViewAnimator viewAnimator, FlipDirection dir) {

        final View fromView = viewAnimator.getCurrentView();
        final int currentIndex = viewAnimator.getDisplayedChild();
        final int nextIndex = (currentIndex + 1) % viewAnimator.getChildCount();

        final View toView = viewAnimator.getChildAt(nextIndex);

        Animation[] animc = AnimationFactory.flipAnimation(fromView, toView, (nextIndex < currentIndex ? dir.theOtherDirection() : dir), 500, null);

        viewAnimator.setOutAnimation(animc[0]);
        viewAnimator.setInAnimation(animc[1]);

        viewAnimator.showNext();
    }

    //////////////


    /**
     * Slide animations to enter a view from left.
     *
     * @param duration     the animation duration in milliseconds
     * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator)
     * @return a slide transition animation
     */
    public static Animation inFromLeftAnimation(long duration, Interpolator interpolator) {
        Animation inFromLeft = new TranslateAnimation(
                Animation.RELATIVE_TO_PARENT, -1.0f, Animation.RELATIVE_TO_PARENT, 0.0f,
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f
        );
        inFromLeft.setDuration(duration);
        inFromLeft.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator); //AccelerateInterpolator
        return inFromLeft;
    }

    /**
     * Slide animations to hide a view by sliding it to the right
     *
     * @param duration     the animation duration in milliseconds
     * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator)
     * @return a slide transition animation
     */
    public static Animation outToRightAnimation(long duration, Interpolator interpolator) {
        Animation outtoRight = new TranslateAnimation(
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, +1.0f,
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f
        );
        outtoRight.setDuration(duration);
        outtoRight.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);
        return outtoRight;
    }

    /**
     * Slide animations to enter a view from right.
     *
     * @param duration     the animation duration in milliseconds
     * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator)
     * @return a slide transition animation
     */
    public static Animation inFromRightAnimation(long duration, Interpolator interpolator) {

        Animation inFromRight = new TranslateAnimation(
                Animation.RELATIVE_TO_PARENT, +1.0f, Animation.RELATIVE_TO_PARENT, 0.0f,
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f
        );
        inFromRight.setDuration(duration);
        inFromRight.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);
        return inFromRight;
    }

    /**
     * Slide animations to hide a view by sliding it to the left.
     *
     * @param duration     the animation duration in milliseconds
     * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator)
     * @return a slide transition animation
     */
    public static Animation outToLeftAnimation(long duration, Interpolator interpolator) {
        Animation outtoLeft = new TranslateAnimation(
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, -1.0f,
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f
        );
        outtoLeft.setDuration(duration);
        outtoLeft.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);
        return outtoLeft;
    }

    /**
     * Slide animations to enter a view from top.
     *
     * @param duration     the animation duration in milliseconds
     * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator)
     * @return a slide transition animation
     */
    public static Animation inFromTopAnimation(long duration, Interpolator interpolator) {
        Animation infromtop = new TranslateAnimation(
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f,
                Animation.RELATIVE_TO_PARENT, -1.0f, Animation.RELATIVE_TO_PARENT, 0.0f
        );
        infromtop.setDuration(duration);
        infromtop.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);
        return infromtop;
    }

    /**
     * Slide animations to hide a view by sliding it to the top
     *
     * @param duration     the animation duration in milliseconds
     * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator)
     * @return a slide transition animation
     */
    public static Animation outToTopAnimation(long duration, Interpolator interpolator) {
        Animation outtotop = new TranslateAnimation(
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f,
                Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, -1.0f
        );
        outtotop.setDuration(duration);
        outtotop.setInterpolator(interpolator == null ? new AccelerateInterpolator() : interpolator);
        return outtotop;
    }

    /**
     * A fade animation that will fade the subject in by changing alpha from 0 to 1.
     *
     * @param duration the animation duration in milliseconds
     * @param delay    how long to wait before starting the animation, in milliseconds
     * @return a fade animation
     * @see #fadeInAnimation(long, View)
     */
    public static Animation fadeInAnimation(long duration, long delay) {

        Animation fadeIn = new AlphaAnimation(0, 1);
        fadeIn.setInterpolator(new DecelerateInterpolator());
        fadeIn.setDuration(duration);
        fadeIn.setStartOffset(delay);

        return fadeIn;
    }

    /**
     * A fade animation that will fade the subject out by changing alpha from 1 to 0.
     *
     * @param duration the animation duration in milliseconds
     * @param delay    how long to wait before starting the animation, in milliseconds
     * @return a fade animation
     * @see #fadeOutAnimation(long, View)
     */
    public static Animation fadeOutAnimation(long duration, long delay) {

        Animation fadeOut = new AlphaAnimation(1, 0);
        fadeOut.setInterpolator(new AccelerateInterpolator());
        fadeOut.setStartOffset(delay);
        fadeOut.setDuration(duration);

        return fadeOut;
    }

    /**
     * A fade animation that will ensure the View starts and ends with the correct visibility
     *
     * @param view     the View to be faded in
     * @param duration the animation duration in milliseconds
     * @return a fade animation that will set the visibility of the view at the start and end of animation
     */
    public static Animation fadeInAnimation(long duration, final View view) {
        Animation animation = fadeInAnimation(500, 0);

        animation.setAnimationListener(new AnimationListener() {
            @Override
            public void onAnimationEnd(Animation animation) {
                view.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }

            @Override
            public void onAnimationStart(Animation animation) {
                view.setVisibility(View.GONE);
            }
        });

        return animation;
    }

    /**
     * A fade animation that will ensure the View starts and ends with the correct visibility
     *
     * @param view     the View to be faded out
     * @param duration the animation duration in milliseconds
     * @return a fade animation that will set the visibility of the view at the start and end of animation
     */
    public static Animation fadeOutAnimation(long duration, final View view) {

        Animation animation = fadeOutAnimation(500, 0);

        animation.setAnimationListener(new AnimationListener() {
            @Override
            public void onAnimationEnd(Animation animation) {
                view.setVisibility(View.GONE);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }

            @Override
            public void onAnimationStart(Animation animation) {
                view.setVisibility(View.VISIBLE);
            }
        });

        return animation;

    }

    /**
     * Creates a pair of animation that will fade in, delay, then fade out
     *
     * @param duration the animation duration in milliseconds
     * @param delay    how long to wait after fading in the subject and before starting the fade out
     * @return a fade in then out animations
     */
    public static Animation[] fadeInThenOutAnimation(long duration, long delay) {
        return new Animation[]{fadeInAnimation(duration, 0), fadeOutAnimation(duration, duration + delay)};
    }

    /**
     * Fades the view in. Animation starts right away.
     *
     * @param v the view to be faded in
     */
    public static void fadeOut(View v) {
        if (v == null) return;
        v.startAnimation(fadeOutAnimation(500, v));
    }

    /**
     * Fades the view out. Animation starts right away.
     *
     * @param v the view to be faded out
     */
    public static void fadeIn(View v) {
        if (v == null) return;

        v.startAnimation(fadeInAnimation(500, v));
    }

    /**
     * Fades the view in, delays the specified amount of time, then fades the view out
     *
     * @param v     the view to be faded in then out
     * @param delay how long the view will be visible for
     */
    public static void fadeInThenOut(final View v, long delay) {
        if (v == null) return;

        v.setVisibility(View.VISIBLE);
        AnimationSet animation = new AnimationSet(true);
        Animation[] fadeInOut = fadeInThenOutAnimation(500, delay);
        animation.addAnimation(fadeInOut[0]);
        animation.addAnimation(fadeInOut[1]);
        animation.setAnimationListener(new AnimationListener() {
            @Override
            public void onAnimationEnd(Animation animation) {
                v.setVisibility(View.GONE);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }

            @Override
            public void onAnimationStart(Animation animation) {
                v.setVisibility(View.VISIBLE);
            }
        });

        v.startAnimation(animation);
    }

} public class AnimationListenerAdapter implements Animation.AnimationListener {

    @Override
    public void onAnimationStart(Animation animation) {
        //implement if necessary
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        //implement if necessary
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
        //implement if necessary
    }
} public class AnimatorListenerAdapter implements  Animator.AnimatorListener{

    @Override
    public void onAnimationStart(Animator animation) {
        //implement if necessary
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        //implement if necessary
    }

    @Override
    public void onAnimationCancel(Animator animation) {
        //implement if necessary
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
        //implement if necessary
    }
} public class API {

    protected static final String PROTOCOL_CHARSET = "utf-8";

    public static void signup(final String email, final String password, final Context context, final NetworkResultListener resultListener) {
        Map<String, String> params = new HashMap<>();
        params.put(SIGNUP_EMAIL_PARAM, email);
        params.put(SIGNUP_PASSWORD_PARAM, password);
        params.put(FIREBASE_INSTANCE_ID_PARAM, Preferences.getFirebaseInstanceId(context));

        VolleySingleton.getInstance(context).getRequestQueue().add(new JsonObjectRequest(Request.Method.POST, SIGNUP_URL, new JSONObject(params), new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                try {
                    String authToken = response.getString(Constants.AUTH_TOKEN_PARAM);
                    Preferences.setAuthToken(context, authToken);
                } catch (JSONException e) {
                    Log.e(API.class, "Parse signup response failed", e);
                }

                if (resultListener != null) {
                    resultListener.onOk();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Response<JSONObject> resp = parseNetworkResponse(error.networkResponse);
                if (resultListener != null) {
                    resultListener.onError(processServerError(resp.result));
                }
            }
        }));
    }

    public static void statistics(final Context context, final NetworkResultListener resultListener) {
        Log.d(API.class, "API.statistics");

        VolleySingleton.getInstance(context).getRequestQueue().add(new BackgroundPreprocessedRequest(Request.Method.GET, STATISTICS_URL, null, null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                Preferences.setUserStatistics(context, Statistics.from(response));
                Intent intent = new Intent(Constants.SYNC_STATISTICS_EVENT);
                PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
                AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
                alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), pendingIntent);
                if (resultListener != null) {
                    resultListener.onOk();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Response<JSONObject> resp = parseNetworkResponse(error.networkResponse);
                if (resultListener != null) {
                    resultListener.onError(processServerError(resp.result));
                }
            }
        }).setHeaders(getHeaders(context)));
    }


    public static void google(String email, String token, final Context context, final NetworkResultListener resultListener) {
        Map<String, String> params = new HashMap<>();
        params.put(GOOGLE_EMAIL_PARAM, email);
        params.put(GOOGLE_TOKEN_PARAM, token);
        params.put(FIREBASE_INSTANCE_ID_PARAM, Preferences.getFirebaseInstanceId(context));

        VolleySingleton.getInstance(context).getRequestQueue().add(new JsonObjectRequest(Request.Method.POST, GOOGLE_URL, new JSONObject(params), new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                try {
                    String authToken = response.getString(Constants.AUTH_TOKEN_PARAM);
                    Preferences.setAuthToken(context, authToken);
                } catch (JSONException e) {
                    Log.e(API.class, "Parse google login response failed", e);
                }
                if (resultListener != null) {
                    resultListener.onOk();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Response<JSONObject> resp = parseNetworkResponse(error.networkResponse);
                if (resultListener != null) {
                    resultListener.onError(processServerError(resp.result));
                }
            }
        }));
    }

    public static void anonymous(String uuid, final Context context, final NetworkResultListener resultListener) {
        Map<String, String> params = new HashMap<>();
        params.put(ANONYMOUS_ID_PARAM, uuid);
        params.put(FIREBASE_INSTANCE_ID_PARAM, Preferences.getFirebaseInstanceId(context));

        VolleySingleton.getInstance(context).getRequestQueue().add(new JsonObjectRequest(Request.Method.POST, ANONYMOUS_URL, new JSONObject(params), new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                try {
                    String authToken = response.getString(Constants.AUTH_TOKEN_PARAM);
                    Preferences.setAuthToken(context, authToken);
                } catch (JSONException e) {
                    Log.e(API.class, "Parse anonymous login response failed", e);
                }
                if (resultListener != null) {
                    resultListener.onOk();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Response<JSONObject> resp = parseNetworkResponse(error.networkResponse);
                if (resultListener != null) {
                    resultListener.onError(processServerError(resp.result));
                }
            }
        }));
    }

    public static void logout(final Context context, final NetworkResultListener resultListener) {
        BackgroundPreprocessedRequest request = new BackgroundPreprocessedRequest(Request.Method.POST, LOGOUT_URL, null, null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                if (resultListener != null) {
                    resultListener.onOk();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Response<JSONObject> resp = parseNetworkResponse(error.networkResponse);
                if (resultListener != null) {
                    resultListener.onError(processServerError(resp.result));
                }
            }
        });

        request.setHeaders(getHeaders(context));
        request.setRetryPolicy(new DefaultRetryPolicy(API_CONNECTION_TIMEOUT, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));

        VolleySingleton.getInstance(context).getRequestQueue().add(request);
    }

    public static void syncUserAsync(final Context context, final Response.Listener<JSONObject> syncListener, ErrorResponseListener errorResponseListener) {
        Log.d(API.class, "API.syncUserAsync");
        BackgroundPreprocessedRequest request = new BackgroundPreprocessedRequest(Request.Method.GET, FETCH_USER_URL, null, new UserFetchResultListener(context, new OnFetchUser() {
            @Override
            public void onFetch(User user) {
                Log.d(API.class, "Fetched ", user.toString(), " user. and procesing it in background thread.");
                List<Rando> dbRandos = RandoDAO.getAllRandos(context);
                int totalUserRandos = user.randosIn.size() + user.randosOut.size();
                if (totalUserRandos != dbRandos.size()
                        || !(dbRandos.containsAll(user.randosIn)
                        && dbRandos.containsAll(user.randosOut))) {
                    RandoDAO.clearRandos(context);
                    RandoDAO.insertRandos(context, user.randosIn);
                    RandoDAO.insertRandos(context, user.randosOut);
                    Notification.sendSyncNotification(context, totalUserRandos, UPDATED);
                } else {
                    Notification.sendSyncNotification(context, totalUserRandos, NOT_UPDATED);
                }
            }
        }), syncListener, errorResponseListener);
        request.setHeaders(getHeaders(context));

        VolleySingleton.getInstance(context).getRequestQueue().add(request);
    }

    public static void uploadImage(final RandoUpload randoUpload, final Context context, final UploadRandoListener uploadListener, final Response.ErrorListener errorListener) {

        VolleyMultipartRequest uploadMultipart = new VolleyMultipartRequest(UPLOAD_RANDO_URL, getHeaders(context), new Response.Listener<NetworkResponse>() {
            @Override
            public void onResponse(NetworkResponse response) {
                Log.d(API.class, "Rando Uploaded Successfully:", randoUpload.toString());
                String resultResponse = new String(response.data);
                if (uploadListener != null) {
                    uploadListener.onUpload(Rando.fromJSON(resultResponse, Rando.Status.OUT));
                }
            }
        }, errorListener) {
            @Override
            protected Map<String, String> getParams() {
                Map<String, String> params = new HashMap<>(2);
                params.put(LATITUDE_PARAM, randoUpload.latitude);
                params.put(LONGITUDE_PARAM, randoUpload.longitude);
                return params;
            }

            @Override
            protected Map<String, DataPart> getByteData() {
                Map<String, DataPart> params = new HashMap<>(1);
                File randoFile = new File(randoUpload.file);
                params.put(IMAGE_PARAM, new DataPart(randoFile.getName(), FileUtil.readFile(randoFile), IMAGE_MIME_TYPE));

                return params;
            }

            @Override
            public Priority getPriority() {
                return Priority.LOW;
            }
        };
        uploadMultipart.setRetryPolicy(new DefaultRetryPolicy(UPLOAD_CONNECTION_TIMEOUT, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));

        VolleySingleton.getInstance(context).getRequestQueue().add(uploadMultipart);
    }

    public static void delete(final String randoId, final Context context, final NetworkResultListener resultListener) throws Exception {
        Log.d(API.class, "Deleting Rando:", randoId);
        BackgroundPreprocessedRequest request = new BackgroundPreprocessedRequest(Request.Method.POST, DELETE_URL + randoId, null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                try {
                    if ("delete".equals(response.getString("command")) &&
                            "done".equals(response.getString("result"))) {
                        Log.d(API.class, "Deleted Rando:", randoId);
                        RandoDAO.deleteRandoByRandoId(context, randoId);
                    }
                } catch (JSONException e) {
                    Log.e(API.class, "Error Deleting Rando", e);
                    resultListener.onError(null);
                }
            }
        }, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                resultListener.onOk();
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(API.class, "Error Deleting Rando", error);
                Response<JSONObject> resp = parseNetworkResponse(error.networkResponse);
                if (resultListener != null) {
                    resultListener.onError(processServerError(resp.result));
                }
            }
        });

        request.setHeaders(getHeaders(context));
        request.setRetryPolicy(new DefaultRetryPolicy(API_CONNECTION_TIMEOUT, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));

        VolleySingleton.getInstance(context).getRequestQueue().add(request);
    }

    public static void report(final String randoId, final Context context, final NetworkResultListener resultListener) throws Exception {
        Log.d(API.class, "Reporting Rando:", randoId);
        BackgroundPreprocessedRequest request = new BackgroundPreprocessedRequest(Request.Method.POST, REPORT_URL + randoId, null, null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                try {
                    if ("report".equals(response.getString("command")) &&
                            "done".equals(response.getString("result"))) {
                        Log.d(API.class, "Reported Rando:", randoId);
                        resultListener.onOk();
                    }
                } catch (JSONException e) {
                    Log.e(API.class, "Error Reporting Rando", e);
                    resultListener.onError(null);
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(API.class, "Error Reporting Rando", error);
                Response<JSONObject> resp = parseNetworkResponse(error.networkResponse);
                if (resultListener != null) {
                    resultListener.onError(processServerError(resp.result));
                }
            }
        });

        request.setHeaders(getHeaders(context));
        request.setRetryPolicy(new DefaultRetryPolicy(API_CONNECTION_TIMEOUT, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));

        VolleySingleton.getInstance(context).getRequestQueue().add(request);
    }

    public static void rate(final String randoId, final Context context, final int rating, final NetworkResultListener reportRandoListener) {
        Log.d(API.class, "Rate Rando:", randoId);
        BackgroundPreprocessedRequest request = new BackgroundPreprocessedRequest(Request.Method.POST, RATE_URL + randoId + "?rating=" + rating, null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                try {
                    if ("rate".equals(response.getString("command")) &&
                            "done".equals(response.getString("result"))) {
                        Log.d(API.class, "Rated Rando:", randoId);
                        Rando rando = RandoDAO.getRandoByRandoId(context, randoId);
                        rando.rating = rating;
                        RandoDAO.updateRando(context, rando);
                    }
                } catch (JSONException e) {
                    Log.e(API.class, "Error Rating Rando", e);
                    reportRandoListener.onError(null);
                }
            }
        }, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                reportRandoListener.onOk();
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(API.class, "Error Rating Rando", error);
                reportRandoListener.onError(null);
            }
        });

        request.setHeaders(getHeaders(context));
        request.setRetryPolicy(new DefaultRetryPolicy(API_CONNECTION_TIMEOUT, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));

        VolleySingleton.getInstance(context).getRequestQueue().add(request);
    }

    private static Map<String, String> getHeaders(Context context) {
        Map<String, String> headers = new HashMap<>(2);
        headers.put(AUTHORIZATION_HEADER, "Token " + Preferences.getAuthToken(context));
        if (!Preferences.getFirebaseInstanceId(context).isEmpty()) {
            headers.put(FIREBASE_INSTANCE_ID_HEADER, Preferences.getFirebaseInstanceId(context));
        }
        return headers;
    }

    private static Error processServerError(JSONObject json) {
        if (json == null) {
            return new Error().setCode(R.string.error_no_network);
        }

        try {
            switch (json.getInt(ERROR_CODE_PARAM)) {
                case Constants.UNAUTHORIZED_CODE:
                    return new Error().setCode(R.string.error_400);
                case Constants.FORBIDDEN_CODE: {
                    String resetTime = "";
                    try {
                        String message = json.getString("message");
                        Matcher matcher = Pattern.compile("\\d+").matcher(message);
                        if (matcher.find()) {
                            long resetTimeUnix = Long.parseLong(matcher.group());
                            resetTime = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(new Date(resetTimeUnix));
                        }
                    } catch (Exception e) {
                        Log.w(API.class, "Error when try build ban message for user: ", e.getMessage());
                    }
                    return new Error().setCode(R.string.error_411).setMessage(resetTime);
                }
                default: {
                    //TODO: implement all code handling in switch and replace server "message" with default value.
                    return new Error().setMessage(json.getString("message"));
                }
            }
        } catch (JSONException exc) {
            return processError(exc);
        }
    }

    private static Error processError(Exception exc) {
        //We don't want to log Connectivity exceptions
        if (exc instanceof UnknownHostException
                || exc instanceof ConnectException) {
            return new Error().setCode(R.string.error_no_network);
        }
        Crashlytics.logException(exc);
        Log.e(API.class, "processError method", exc);
        return new Error().setCode(R.string.error_unknown_err);
    }

    public static Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
        if (response == null || response.data == null) {
            return Response.error(new ParseError(new Exception("Unknown error or no network")));
        }

        try {
            String jsonString = new String(response.data,
                    HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
            return Response.success(new JSONObject(jsonString),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JSONException je) {
            return Response.error(new ParseError(je));
        }
    }

} public class Error {
    public String message;
    public int code;

    public Error setMessage(String message) {
        this.message = message;
        return this;
    }

    public Error setCode(int code) {
        this.code = code;
        return this;
    }

    public String buildMessage(Context context) {
        String finalMessage = "";
        if (this.code != 0) {
            finalMessage = context.getString(code);
        }

        if (this.message != null) {
            finalMessage += " " + this.message;
        }
        return finalMessage;
    }
} public abstract class NetworkResultListener {

    protected Context context;

    public NetworkResultListener(Context context) {
        this.context = context;
    }

    public abstract void onOk();

    public void onError(Error error) {
        if (error.code == R.string.error_400) {
            Intent intent = new Intent(context, AuthActivity.class);
            intent.putExtra(Constants.LOGOUT_ACTIVITY, true);
            context.startActivity(intent);
            return;
        }
        this.onFail(error);
    }

    protected abstract void onFail(Error error);
} public class RandoDAO {

    private RandoDAO() {
    }

    public static synchronized RandoUpload addToUpload(final Context context, RandoUpload randoUpload) {
        if (randoUpload.file == null || randoUpload.date == null) {
            return null;
        }

        ContentValues values = new ContentValues();
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_FILE, randoUpload.file);
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_LATITUDE, randoUpload.latitude);
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_LONGITUDE, randoUpload.longitude);
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_DATE, randoUpload.date.getTime());
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_LAST_TRY_DATE, randoUpload.lastTry.getTime());

        long id = getDB(context).insert(RandoDBHelper.RandoUploadTable.NAME, null, values);

        if (id > 0) {
            randoUpload.id = (int) id;
            return randoUpload;
        } else {
            return null;
        }
    }

    public static synchronized int countAllRandosToUpload(final Context context) {
        Cursor cursor = getDB(context).query(RandoDBHelper.RandoUploadTable.NAME,
                RandoDBHelper.RandoUploadTable.ALL_COLUMNS, null, null, null, null, null, null);
        int result = cursor.getCount();
        cursor.close();
        return result;
    }

    public static synchronized RandoUpload getNextRandoToUpload(final Context context) {
        String dateS = String.valueOf(System.currentTimeMillis() + Constants.UPLOAD_RETRY_TIMEOUT);
        Cursor cursor = getDB(context).query(RandoDBHelper.RandoUploadTable.NAME,
                RandoDBHelper.RandoUploadTable.ALL_COLUMNS, RandoDBHelper.RandoUploadTable.COLUMN_LAST_TRY_DATE
                        + "<" + dateS, null, null, null, RandoDBHelper.RandoUploadTable.COLUMN_DATE + " ASC", "1");

        cursor.moveToFirst();
        RandoUpload rando = null;
        if (!cursor.isAfterLast()) {
            rando = new RandoUpload();
            rando.id = cursor.getInt(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoUploadTable.COLUMN_ID));
            rando.file = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoUploadTable.COLUMN_FILE));
            rando.latitude = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoUploadTable.COLUMN_LATITUDE));
            rando.longitude = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoUploadTable.COLUMN_LONGITUDE));
            long userDate = cursor.getLong(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoUploadTable.COLUMN_DATE));
            long lastTryDate = cursor.getLong(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoUploadTable.COLUMN_LAST_TRY_DATE));
            if (userDate > 0) {
                rando.date = new Date(userDate);
            } else {
                rando.date = new Date();
            }
            if (lastTryDate > 0) {
                rando.lastTry = new Date(lastTryDate);
            } else {
                rando.lastTry = new Date(0);
            }
            cursor.close();
        }
        return rando;
    }

    public static synchronized void updateRandoToUpload(final Context context, RandoUpload rando) {
        ContentValues values = new ContentValues();
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_FILE, rando.file);
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_LATITUDE, rando.latitude);
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_LONGITUDE, rando.longitude);
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_DATE, rando.date.getTime());
        values.put(RandoDBHelper.RandoUploadTable.COLUMN_LAST_TRY_DATE, rando.lastTry.getTime());

        getDB(context).update(RandoDBHelper.RandoUploadTable.NAME, values, RandoDBHelper.RandoTable.COLUMN_ID + " = " + rando.id, null);
    }

    /**
     * Removes RandoToUpload by id
     *
     * @param id of item to remove
     */
    public static synchronized void deleteRandoToUploadById(final Context context, int id) {
        getDB(context).delete(RandoDBHelper.RandoUploadTable.NAME, RandoDBHelper.RandoUploadTable.COLUMN_ID + " = " + id, null);
        Log.i(RandoDAO.class, "Rando to upload deleted with id: ", String.valueOf(id));
    }

    /**
     * Removes all rows from RandoToUpload table
     */

    public static synchronized void clearRandoToUpload(final Context context) {
        getDB(context).delete(RandoDBHelper.RandoUploadTable.NAME, "", null);
        Log.i(RandoDAO.class, "RandosToUpload cleared");
    }

    /**
     * Creates rando and returns instance of created rando.
     *
     * @param rando Rando to insert
     * @return returns instance of created rando or null
     */

    public static synchronized Rando createRando(final Context context, Rando rando) {
        if (rando == null) {
            return null;
        }
        ContentValues values = randoToContentValues(rando);
        long insertId = getDB(context).insert(RandoDBHelper.RandoTable.NAME, null,
                values);
        return getRandoById(context, insertId);
    }

    /**
     * Creates rando and returns instance of created rando.
     *
     * @param rando Rando to insert
     * @return returns instance of created rando or null
     */

    public static synchronized Rando createOrUpdateRandoCheckingByRandoId(final Context context, Rando rando) {
        if (rando == null) {
            return null;
        }
        Rando dbRando = getRandoByRandoId(context, rando.randoId);
        long dbId;
        if (dbRando != null) {
            rando.id = dbRando.id;
            dbId = dbRando.id;
            updateRando(context, rando);
        } else {
            ContentValues values = randoToContentValues(rando);
            dbId = getDB(context).insert(RandoDBHelper.RandoTable.NAME, null,
                    values);
        }
        return getRandoById(context, dbId);
    }

    /**
     * Finds rando instance.
     *
     * @param id Rando id to get
     * @return Rando instance or null if rando hasn't been found
     */

    public static synchronized Rando getRandoById(final Context context, long id) {
        Cursor cursor = getDB(context).query(RandoDBHelper.RandoTable.NAME,
                null, RandoDBHelper.RandoTable.COLUMN_ID + " = " + id, null,
                null, null, null);
        cursor.moveToFirst();
        Rando newRando = cursorToRando(cursor);
        cursor.close();
        return newRando;
    }

    /**
     * Finds rando instance.
     *
     * @param randoId Rando id to get
     * @return Rando instance or null if rando hasn't been found
     */

    public static synchronized Rando getRandoByRandoId(final Context context, String randoId) {
        Cursor cursor = getDB(context).query(RandoDBHelper.RandoTable.NAME,
                null, RandoDBHelper.RandoTable.COLUMN_USER_RANDO_ID + " = '" + randoId + "'", null,
                null, null, null);
        cursor.moveToFirst();
        Rando rando = cursorToRando(cursor);
        cursor.close();
        return rando;
    }

    /**
     * Deletes rando instance from DB by randoId.
     *
     * @param randoId randoId
     */
    public static synchronized void deleteRandoByRandoId(final Context context, String randoId) {
        getDB(context).delete(RandoDBHelper.RandoTable.NAME, RandoDBHelper.RandoTable.COLUMN_USER_RANDO_ID
                + " = '" + randoId + "'", null);
        Log.i(RandoDAO.class, "Rando deleted with randoId: ", String.valueOf(randoId));
    }

    /**
     * clear rando pairs Table in DB.
     */
    public static synchronized void clearRandos(final Context context) {
        getDB(context).delete(RandoDBHelper.RandoTable.NAME, "", null);
        Log.i(RandoDAO.class, "Randos table cleared");
    }

    /**
     * Updates rando instance from DB.
     *
     * @param rando Rando to update
     */
    public static synchronized void updateRando(final Context context, Rando rando) {
        long id = rando.id;

        ContentValues values = randoToContentValues(rando);

        getDB(context).update(RandoDBHelper.RandoTable.NAME, values, RandoDBHelper.RandoTable.COLUMN_ID + " = " + id, null);
        Log.w(RandoDAO.class, "Rando updated with id: ", String.valueOf(id));
    }

    /**
     * @return all rando instances found in DB
     */
    // ToDo: replace with cursor solution
    public static synchronized List<Rando> getAllRandos(final Context context) {
        List<Rando> randos = new LinkedList<>();

        Cursor cursor = getDB(context).query(RandoDBHelper.RandoTable.NAME,
                null, null, null, null, null, RandoDBHelper.RandoTable.COLUMN_USER_RANDO_DATE + " DESC", null);

        cursor.moveToFirst();
        while (!cursor.isAfterLast()) {
            Rando rando = cursorToRando(cursor);
            randos.add(rando);
            cursor.moveToNext();
        }
        cursor.close();

        return randos;
    }

    public static synchronized Cursor getCursor(final Context context, boolean isStranger) {

        if (isStranger) {
            return getDB(context).query(RandoDBHelper.RandoTable.NAME,
                    null, RandoDBHelper.RandoTable.COLUMN_RANDO_STATUS
                            + " = '" + Rando.Status.IN.name() + "'", null, null, null, RandoDBHelper.RandoTable.COLUMN_USER_RANDO_DATE + " DESC", null);
        }
        else {
            // randos to upload
            Cursor cursor1 = getDB(context).query(RandoDBHelper.RandoUploadTable.NAME,
                    RandoDBHelper.RandoUploadTable.ALL_COLUMNS, null, null, null, null, RandoDBHelper.RandoUploadTable.COLUMN_DATE + " " + "DESC", null);

            // uploaded randos
            Cursor cursor2 = getDB(context).query(RandoDBHelper.RandoTable.NAME,
                    null, RandoDBHelper.RandoTable.COLUMN_RANDO_STATUS
                            + " = '" + Rando.Status.OUT.name() + "'", null, null, null, RandoDBHelper.RandoTable.COLUMN_USER_RANDO_DATE + " DESC", null);


            return new MergeCursor(new Cursor[] {cursor1, cursor2});
        }

    }

    /**
     * Inserts list of randos into DB
     *
     * @param randos Rando list to insert
     */
    public static synchronized void insertRandos(final Context context, List<Rando> randos) {
        getDB(context).beginTransaction();
        try {
            for (Rando rando : randos) {
                createRando(context, rando);
            }
            getDB(context).setTransactionSuccessful();
        } finally {
            getDB(context).endTransaction();
        }
    }

    private static synchronized SQLiteDatabase getDB(final Context context) {
        return RandoDBHelper.getDatabase(context);
    }

    /**
     * Extracts Rando object from Cursor object
     *
     * @param cursor Cursor pointing to row
     * @return rando object extracted from cursor
     */

    public static Rando cursorToRando(Cursor cursor) {
        if (cursor.getCount() == 0) {
            return null;
        } else {
            Rando rando = new Rando();
            rando.id = cursor.getInt(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_ID));

            rando.randoId = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_ID));
            rando.imageURL = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_URL));
            rando.imageURLSize.small = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_URL_SMALL));
            rando.imageURLSize.medium = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_URL_MEDIUM));
            rando.imageURLSize.large = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_URL_LARGE));
            long userDate = cursor.getLong(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_DATE));
            rando.date = userDate != 0 ? new Date(userDate) : null;
            rando.mapURL = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_MAP_URL));
            rando.mapURLSize.small = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_MAP_URL_SMALL));
            rando.mapURLSize.medium = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_MAP_URL_MEDIUM));
            rando.mapURLSize.large = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_USER_MAP_URL_LARGE));
            rando.status = Rando.Status.valueOf(cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_RANDO_STATUS)));
            rando.detected = cursor.getString(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_DETECTED));
            rando.rating = cursor.getInt(cursor.getColumnIndexOrThrow(RandoDBHelper.RandoTable.COLUMN_RATING));
            return rando;
        }
    }

    /**
     * Converts Rando object into ContentValues object
     *
     * @param rando Rando to convert
     * @return ContentValues representing rando
     */
    private static ContentValues randoToContentValues(Rando rando) {
        ContentValues values = new ContentValues();

        values.put(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_ID, rando.randoId);
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_URL, rando.imageURL);
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_URL_SMALL, rando.imageURLSize.small);
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_URL_MEDIUM, rando.imageURLSize.medium);
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_URL_LARGE, rando.imageURLSize.large);
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_RANDO_DATE, rando.date.getTime());
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_MAP_URL, rando.mapURL);
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_MAP_URL_SMALL, rando.mapURLSize.small);
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_MAP_URL_MEDIUM, rando.mapURLSize.medium);
        values.put(RandoDBHelper.RandoTable.COLUMN_USER_MAP_URL_LARGE, rando.mapURLSize.large);
        values.put(RandoDBHelper.RandoTable.COLUMN_RANDO_STATUS, rando.status.name());
        if (rando.detected != null) {
            values.put(RandoDBHelper.RandoTable.COLUMN_DETECTED, rando.detected);
        }
        if (rando.rating != null) {
            values.put(RandoDBHelper.RandoTable.COLUMN_RATING, rando.rating);
        }

        return values;
    }
} public class RandoDBHelper extends SQLiteOpenHelper {

    private static final int DATABASE_VERSION = 12;
    private static final String DATABASE_NAME = "rando.db";

    private static RandoDBHelper helperInstance;
    private SQLiteDatabase db;

    private String LOG_TAG = "RandoDBHelper";

    private RandoDBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    private static synchronized RandoDBHelper getInstance(Context context) {
        if (helperInstance == null) {
            helperInstance = new RandoDBHelper(context);
        }
        return helperInstance;
    }

    public static synchronized SQLiteDatabase getDatabase(Context context) {
        getInstance(context);
        if (null == helperInstance.db) {
            helperInstance.db = helperInstance.getWritableDatabase();
        }
        return helperInstance.db;
    }

    @Override
    protected void finalize() throws Throwable {
        if (null != helperInstance)
            helperInstance.close();
        if (null != db)
            db.close();
        super.finalize();
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(RandoTable.CREATE_TABLE_SQL);
        db.execSQL(RandoUploadTable.CREATE_TABLE_SQL);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d(LOG_TAG,
                "Upgrading database from version " + oldVersion + " to "
                        + newVersion + ", which will destroy all old data");
        if (oldVersion < 9) {
            Log.d(LOG_TAG, "Upgrading database from prior 9 version");
            dropAllAndCreate(db);
            return;
        }
        if (oldVersion < 10) {
            upgradeTo10(db);
        }

        if (oldVersion < 11) {
            upgradeTo11(db);
        }

        if (oldVersion < 12) {
            upgradeTo12(db);
        }
    }

    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d(LOG_TAG,
                "Downgrading database from version " + oldVersion + " to "
                        + newVersion + ", which will destroy all old data");
        dropAllAndCreate(db);
    }

    private void upgradeTo10(SQLiteDatabase db) {
        Log.d(LOG_TAG, "Upgrading database from prior 10 version");
        db.execSQL("ALTER TABLE " + RandoTable.NAME + " ADD COLUMN " + RandoTable.COLUMN_DETECTED + " TEXT");
    }

    private void upgradeTo11(SQLiteDatabase db) {
        Log.d(LOG_TAG, "Upgrading database from prior 11 version");
        if (!isRandoTableColumnExist(db, RandoTable.COLUMN_RANDO_STATUS)) {
            dropAllAndCreate(db);
        }
    }

    private void upgradeTo12(SQLiteDatabase db) {
        Log.d(LOG_TAG, "Upgrading database from prior 12 version");
        db.execSQL("ALTER TABLE " + RandoTable.NAME + " ADD COLUMN " + RandoTable.COLUMN_RATING + " INTEGER DEFAULT 0");
    }

    private void dropAllAndCreate(SQLiteDatabase db) {
        Log.d(LOG_TAG, "Drop all tables and recreate which will destroy all old data");
        db.execSQL("DROP TABLE IF EXISTS " + RandoTable.NAME);
        db.execSQL("DROP TABLE IF EXISTS " + RandoUploadTable.NAME);
        onCreate(db);
    }

    private boolean isRandoTableColumnExist(SQLiteDatabase db, String columnName) {
        Cursor c = null;
        try {
            c = db.rawQuery("select * from " + RandoTable.NAME + " limit 1", null);
            if (c != null && c.getColumnIndex(columnName) > 0) {
                return true;
            }
        } catch (Exception e) {
            Log.e(RandoDBHelper.class.toString(), e.getMessage(), e);
        } finally {
            if (c != null)
                c.close();
        }
        return false;
    }


    public static class RandoTable {
        public static final String NAME = "rando";

        public static final String COLUMN_ID = "_id";
        public static final String COLUMN_USER_RANDO_ID = "USER_RANDO_ID";
        public static final String COLUMN_USER_RANDO_URL = "USER_RANDO_URL";
        public static final String COLUMN_USER_RANDO_URL_SMALL = "USER_RANDO_URL_SMALL";
        public static final String COLUMN_USER_RANDO_URL_MEDIUM = "USER_RANDO_URL_MEDIUM";
        public static final String COLUMN_USER_RANDO_URL_LARGE = "USER_RANDO_URL_LARGE";
        public static final String COLUMN_USER_RANDO_DATE = "USER_RANDO_DATE";
        public static final String COLUMN_USER_MAP_URL = "USER_MAP_URL";
        public static final String COLUMN_USER_MAP_URL_SMALL = "USER_MAP_URL_SMALL";
        public static final String COLUMN_USER_MAP_URL_MEDIUM = "USER_MAP_URL_MEDIUM";
        public static final String COLUMN_USER_MAP_URL_LARGE = "USER_MAP_URL_LARGE";
        public static final String COLUMN_DETECTED = "DETECTED";
        public static final String COLUMN_RATING = "RATING";

        public static final String COLUMN_RANDO_STATUS = "RANDO_STATUS";

        public static final String CREATE_TABLE_SQL = "CREATE TABLE " + RandoTable.NAME +
                " (" + COLUMN_ID + " integer primary key autoincrement, " +
                COLUMN_USER_RANDO_ID + " text," +
                COLUMN_USER_RANDO_URL + " text," +
                COLUMN_USER_RANDO_URL_SMALL + " text," +
                COLUMN_USER_RANDO_URL_MEDIUM + " text," +
                COLUMN_USER_RANDO_URL_LARGE + " text," +
                COLUMN_USER_RANDO_DATE + " integer," +
                COLUMN_USER_MAP_URL + " text," +
                COLUMN_USER_MAP_URL_SMALL + " text," +
                COLUMN_USER_MAP_URL_MEDIUM + " text," +
                COLUMN_USER_MAP_URL_LARGE + " text," +
                COLUMN_RANDO_STATUS + " text," +
                COLUMN_RATING + " integer DEFAULT 0," +
                COLUMN_DETECTED + " text" +
                ");";
    }


    public static class RandoUploadTable {
        public static final String NAME = "rando_upload";

        public static final String COLUMN_ID = "_id";
        public static final String COLUMN_FILE = "RANDO_FILE";
        public static final String COLUMN_LATITUDE = "RANDO_LATITUDE";
        public static final String COLUMN_LONGITUDE = "RANDO_LONGITUDE";
        public static final String COLUMN_DATE = "RANDO_DATE";
        public static final String COLUMN_LAST_TRY_DATE = "RANDO_LAST_TRY_DATE";

        public static final String[] ALL_COLUMNS = {
                COLUMN_ID,
                "'"+Constants.TO_UPLOAD_RANDO_ID+"' AS " + RandoTable.COLUMN_USER_RANDO_ID,
                COLUMN_FILE,
                COLUMN_FILE +" AS " + RandoTable.COLUMN_USER_RANDO_URL,
                COLUMN_FILE+" AS " + RandoTable.COLUMN_USER_RANDO_URL_MEDIUM,
                COLUMN_FILE+" AS " + RandoTable.COLUMN_USER_RANDO_URL_SMALL,
                COLUMN_FILE+" AS " + RandoTable.COLUMN_USER_RANDO_URL_LARGE,
                COLUMN_DATE+" AS " + RandoTable.COLUMN_USER_RANDO_DATE,
                "'' AS " + RandoTable.COLUMN_USER_MAP_URL,
                "'' AS " + RandoTable.COLUMN_USER_MAP_URL,
                "'' AS " + RandoTable.COLUMN_USER_MAP_URL_SMALL,
                "'' AS " + RandoTable.COLUMN_USER_MAP_URL_MEDIUM,
                "'' AS " + RandoTable.COLUMN_USER_MAP_URL_LARGE,
                "'"+ Rando.Status.OUT+"' AS " + RandoTable.COLUMN_RANDO_STATUS,
                "'' AS " + RandoTable.COLUMN_DETECTED,
                "0 AS " + RandoTable.COLUMN_RATING,

                COLUMN_LATITUDE,
                COLUMN_LONGITUDE,
                COLUMN_DATE,
                COLUMN_LAST_TRY_DATE};

        private static final String CREATE_TABLE_SQL = "CREATE TABLE " + RandoUploadTable.NAME +
                " (" + COLUMN_ID + " integer primary key autoincrement, " +
                COLUMN_FILE + " text," +
                COLUMN_LATITUDE + " text," +
                COLUMN_LONGITUDE + " text," +
                COLUMN_DATE + " integer," +
                COLUMN_LAST_TRY_DATE + " integer" +
                ");";
    }
} public class Rando implements Serializable {

    public enum Status {
        IN, OUT
    }

    public int id;
    public String randoId;
    public String imageURL;
    public UrlSize imageURLSize = new UrlSize();
    public Date date;
    public String mapURL;
    public UrlSize mapURLSize = new UrlSize();
    public Status status;
    public String detected;
    public Integer rating;

    public boolean isToUpload() {
        return Constants.TO_UPLOAD_RANDO_ID.equals(randoId);
    }

    public boolean isUnwanted() {
        return detected != null && detected.contains("\"unwanted\"");
    }

    public static Rando fromJSON(String jsonRandoString, Rando.Status status) {
        try {
            return fromJSON(new JSONObject(jsonRandoString), status);
        } catch (JSONException e){
            return null;
        }
    }

    public static Rando fromJSON(JSONObject jsonRando, Rando.Status status) throws JSONException {
        Rando rando = new Rando();
        JSONObject userRandoUrlSizes = jsonRando.getJSONObject(IMAGE_URL_SIZES_PARAM);
        JSONObject userMapUrlSizes = jsonRando.getJSONObject(MAP_URL_SIZES_PARAM);

        rando.randoId = jsonRando.getString(RANDO_ID_PARAM);
        rando.imageURL = jsonRando.getString(IMAGE_URL_PARAM);
        rando.status = status;
        rando.imageURLSize.small = userRandoUrlSizes.getString(SMALL_PARAM);
        rando.imageURLSize.medium = userRandoUrlSizes.getString(MEDIUM_PARAM);
        rando.imageURLSize.large = userRandoUrlSizes.getString(LARGE_PARAM);

        rando.mapURL = jsonRando.getString(MAP_URL_PARAM);
        rando.mapURLSize.small = userMapUrlSizes.getString(SMALL_PARAM);
        rando.mapURLSize.medium = userMapUrlSizes.getString(MEDIUM_PARAM);
        rando.mapURLSize.large = userMapUrlSizes.getString(LARGE_PARAM);

        rando.date = new Date(jsonRando.getLong(CREATION_PARAM));

        if (jsonRando.has(DETECTED_PARAM)){
            JSONArray detected = jsonRando.getJSONArray(DETECTED_PARAM);
            rando.detected = detected.join(",");
        }

        if (jsonRando.has(RATING_PARAM)){
            rando.rating = jsonRando.getInt(RATING_PARAM);
        }
        return rando;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Rando rando = (Rando) o;

        if (imageURL != null ? !imageURL.equals(rando.imageURL) : rando.imageURL != null)
            return false;
        if (imageURLSize != null ? !imageURLSize.equals(rando.imageURLSize) : rando.imageURLSize != null)
            return false;
        if (mapURL != null ? !mapURL.equals(rando.mapURL) : rando.mapURL != null) return false;
        if (mapURLSize != null ? !mapURLSize.equals(rando.mapURLSize) : rando.mapURLSize != null)
            return false;
        if (randoId != null ? !randoId.equals(rando.randoId) : rando.randoId != null) return false;
        if (status != rando.status) return false;
        if (detected != null ? !detected.equals(rando.detected) : rando.detected != null)
            return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = randoId != null ? randoId.hashCode() : 0;
        result = 31 * result + (imageURL != null ? imageURL.hashCode() : 0);
        result = 31 * result + (imageURLSize != null ? imageURLSize.hashCode() : 0);
        result = 31 * result + (mapURL != null ? mapURL.hashCode() : 0);
        result = 31 * result + (mapURLSize != null ? mapURLSize.hashCode() : 0);
        result = 31 * result + (status != null ? status.hashCode() : 0);
        result = 31 * result + (detected != null ? detected.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Rando{" +
                "id=" + id +
                ", randoId='" + randoId + '\'' +
                ", imageURL='" + imageURL + '\'' +
                ", imageURLSize=" + imageURLSize +
                ", date=" + date +
                ", mapURL='" + mapURL + '\'' +
                ", mapURLSize=" + mapURLSize +
                ", status=" + status +
                ", detected='" + detected + '\'' +
                ", rating=" + rating +
                ", toUpload=" + isToUpload() +
                '}';
    }

    public String getBestImageUrlBySize(int size) {
        return getBestUrlBySize(size, imageURLSize);
    }

    public String getBestMapUrlBySize(int size) {
        return getBestUrlBySize(size, mapURLSize);
    }

    private String getBestUrlBySize(int imageSize, Rando.UrlSize urls) {
        if (imageSize >= Constants.SIZE_LARGE) {
            return urls.large;
        } else if (imageSize >= Constants.SIZE_MEDIUM) {
            return urls.medium;
        }
        return urls.small;
    }



    public class UrlSize implements Serializable {
        public String small;
        public String medium;
        public String large;

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof UrlSize)) return false;

            UrlSize urlSize = (UrlSize) o;

            if (large != null ? !large.equals(urlSize.large) : urlSize.large != null)
                return false;
            if (medium != null ? !medium.equals(urlSize.medium) : urlSize.medium != null)
                return false;
            if (small != null ? !small.equals(urlSize.small) : urlSize.small != null)
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = small != null ? small.hashCode() : 0;
            result = 31 * result + (medium != null ? medium.hashCode() : 0);
            result = 31 * result + (large != null ? large.hashCode() : 0);
            return result;
        }
    }

    public boolean isMapEmpty() {
        return mapURLSize == null || (TextUtils.isEmpty(mapURLSize.large) && TextUtils.isEmpty(mapURLSize.medium) && TextUtils.isEmpty(mapURLSize.small));
    }

    public static class DateComparator implements Comparator<Rando> {

        @Override
        public int compare(Rando lhs, Rando rhs) {
            Log.d(Rando.DateComparator.class, "Compare date: ", Long.toString(rhs.date.getTime()), " == ", Long.toString(lhs.date.getTime()), "  > ", Integer.toString((int) (rhs.date.getTime() - lhs.date.getTime())));
            return (int) (rhs.date.getTime() - lhs.date.getTime());
        }
    }
} public class Log {

    public static void i(Class clazz, String... msgs) {
        android.util.Log.i(clazz.getName(), concatenate(msgs));
    }

    public static void d(Class clazz, String... msgs) {
        android.util.Log.d(clazz.getName(), concatenate(msgs));
    }

    public static void w(Class clazz, String... msgs) {
        android.util.Log.w(clazz.getName(), concatenate(msgs));
    }

    public static void e(Class clazz, String... msgs) {
        android.util.Log.e(clazz.getName(), concatenate(msgs));
    }

    public static void e(Class clazz, String comment, Throwable throwable) {
        android.util.Log.e(clazz.getName(), comment + " error:", throwable);
    }

    public static void v(Class clazz, String... msgs) {
        android.util.Log.v(clazz.getName(), concatenate(msgs));
    }

    private static String concatenate(String[] msgs) {
        if (msgs == null) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        for (String msg : msgs) {
            sb.append(msg).append(" ");
        }
        return sb.toString();
    }

} public class VolleySingleton {

    private static VolleySingleton instance = null;

    private RequestQueue requestQueue;
    private ImageLoader imageLoader;

    private VolleySingleton(Context context) {
        requestQueue = createRequestQueue(context);
        imageLoader = new ImageLoader(this.requestQueue, new LruMemCache());
    }

    public static VolleySingleton getInstance(Context context) {
        if (instance == null) {
            instance = new VolleySingleton(context);
        }
        return instance;
    }

    public RequestQueue getRequestQueue() {
        return requestQueue;
    }

    public ImageLoader getImageLoader() {
        return imageLoader;
    }

    private RequestQueue createRequestQueue(Context context) {
        if (context == null) {
            return null;
        }

        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
        Network network = new BasicNetwork(new HurlStack());
        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir, DEFAULT_CACHE_SIZE), network);
        queue.start();
        return queue;
    }

} public class Preferences {
    public static final String AUTH_TOKEN_DEFAULT_VALUE = "";
    public static final String FIREBASE_INSTANCE_ID_DEFAULT_VALUE = "";
    public static final String ACCOUNT_DEFAULT_VALUE = "";
    public static final int STATISTICS_DEFAULT_VALUE = 0;

    private static Object monitor = new Object();

    public static String getAuthToken(Context context) {
        synchronized (monitor) {
            return getSharedPreferences(context).getString(AUTH_TOKEN, AUTH_TOKEN_DEFAULT_VALUE);
        }
    }

    public static void setAuthToken(Context context, String token) {
        if (token != null) {
            synchronized (monitor) {
                getSharedPreferences(context).edit().putString(AUTH_TOKEN, token).apply();
            }
        }
    }

    public static void removeAuthToken(Context context) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().remove(AUTH_TOKEN).apply();
        }
    }


    public static String getAccount(Context context) {
        synchronized (monitor) {
            return getSharedPreferences(context).getString(ACCOUNT, ACCOUNT_DEFAULT_VALUE);
        }
    }

    public static void setAccount(Context context, String token) {
        if (token != null) {
            synchronized (monitor) {
                getSharedPreferences(context).edit().putString(ACCOUNT, token).apply();
            }
        }
    }

    public static void removeAccount(Context context) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().remove(ACCOUNT).apply();
        }
    }

    public static Location getLocation(Context context) {
        Location location = new Location(LOCATION);
        synchronized (monitor) {
            double lat = Double.valueOf(getSharedPreferences(context).getString(LATITUDE_PARAM, "0"));
            double lon = Double.valueOf(getSharedPreferences(context).getString(LONGITUDE_PARAM, "0"));
            location.setLatitude(lat);
            location.setLongitude(lon);
        }
        return location;
    }

    public static void setLocation(Context context, Location location) {
        if (location != null) {
            synchronized (monitor) {
                getSharedPreferences(context).edit().putString(LONGITUDE_PARAM, String.valueOf(location.getLongitude())).apply();
                getSharedPreferences(context).edit().putString(LATITUDE_PARAM, String.valueOf(location.getLatitude())).apply();
            }
        }
    }

    public static void removeLocation(Context context) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().remove(LONGITUDE_PARAM).apply();
            getSharedPreferences(context).edit().remove(LATITUDE_PARAM).apply();
        }
    }

    public static boolean isTrainingFragmentShown() {
        //TODO: change to return real value when Training will be Implemented.
        return true;
        //return 1 == getSharedPreferences().getInt(Constants.TRAINING_FRAGMENT_SHOWN, 0);
    }

    public static void setTrainingFragmentShown(Context context, int i) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().putInt(TRAINING_FRAGMENT_SHOWN, i).apply();
        }
    }

    public static void removeTrainingFragmentShown(Context context) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().remove(TRAINING_FRAGMENT_SHOWN).apply();
        }
    }

    public static void setBanResetAt(Context context, long resetAt) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().putLong(BAN_RESET_AT, resetAt).apply();
        }
    }

    public static long getBanResetAt(Context context) {
        synchronized (monitor) {
            return getSharedPreferences(context).getLong(BAN_RESET_AT, 0L);
        }
    }

    private static SharedPreferences getSharedPreferences(Context context) {
        synchronized (monitor) {
            return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
        }
    }

    public static String getFirebaseInstanceId(Context context) {
        synchronized (monitor) {
            return getSharedPreferences(context).getString(FIREBASE_INSTANCE_ID, FIREBASE_INSTANCE_ID_DEFAULT_VALUE);
        }
    }

    public static void setFirebaseInstanceId(Context context, String token) {
        if (token != null) {
            synchronized (monitor) {
                getSharedPreferences(context).edit().putString(FIREBASE_INSTANCE_ID, token).apply();
            }
        }
    }

    public static void removeFirebaseInstanceId(Context context) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().remove(FIREBASE_INSTANCE_ID).apply();
        }
    }

    public static Facing getCameraFacing(Context context) {
        synchronized (monitor) {
            Facing facing = Facing.valueOf(getSharedPreferences(context).getString(CAMERA_FACING_STRING, Facing.BACK.name()));
            return facing;
        }
    }

    public static void setCameraFacing(Context context, Facing facing) {
        synchronized (monitor) {
            if (facing != null) {
                getSharedPreferences(context).edit().putString(CAMERA_FACING_STRING, facing.name()).apply();
            }
        }
    }

    public static Grid getCameraGrid(Context context) {
        synchronized (monitor) {
            return Grid.valueOf(getSharedPreferences(context).getString(CAMERA_GRID_STRING, Grid.OFF.name()));
        }
    }

    public static void setCameraGrid(Context context, Grid cameraGrid) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().putString(CAMERA_GRID_STRING, cameraGrid.name()).apply();
        }
    }

    public static Flash getCameraFlashMode(Context context, Facing facing) {
        synchronized (monitor) {
            if (facing != null) {
                return Flash.valueOf(getSharedPreferences(context).getString(CAMERA_FLASH_MODE + facing.name(), Flash.OFF.name()));
            } else {
                return Flash.OFF;
            }
        }
    }

    public static void setCameraFlashMode(Context context, Facing facing, Flash flashMode) {
        synchronized (monitor) {
            if (flashMode != null && facing != null) {
                getSharedPreferences(context).edit().putString(CAMERA_FLASH_MODE + facing.name(), flashMode.name()).apply();
            }
        }
    }

    public static void removeCameraFlashMode(Context context, Facing facing) {
        synchronized (monitor) {
            getSharedPreferences(context).edit().remove(CAMERA_FLASH_MODE + facing.name()).apply();
        }
    }

    public static void setUserStatistics(Context context, Statistics statistics) {
        synchronized (monitor) {
            if (statistics != null) {
                getSharedPreferences(context).edit().putInt(USER_STATISTICS_LIKES, statistics.getLikes()).apply();
                getSharedPreferences(context).edit().putInt(USER_STATISTICS_DISLIKES, statistics.getDislikes()).apply();
            }
        }
    }

    public static Statistics getUserStatistics(Context context) {
        synchronized (monitor) {
            return Statistics.of(
                    getSharedPreferences(context).getInt(USER_STATISTICS_LIKES, STATISTICS_DEFAULT_VALUE),
                    getSharedPreferences(context).getInt(USER_STATISTICS_DISLIKES, STATISTICS_DEFAULT_VALUE));
        }
    }
} public class Analytics {

    private static String TAKE_RANDO = "take_rando";
    private static String UPLOAD_RANDO = "upload_rando";
    private static String SHARE_RANDO = "share_rando";
    private static String DELETE_RANDO = "delete_rando";
    private static String REPORT_RANDO = "report_rando";
    private static String TAP_STRANGER_RANDO = "tap_stranger_rando";
    private static String TAP_OWN_RANDO = "tap_own_rando";
    private static String FORCE_SYNC = "force_sync";
    private static String LOGOUT = "logout";
    private static String LOGIN_SKIP = "login_skip";
    private static String LOGIN_GOOGLE = "login_google";
    private static String LOGIN_EMAIL = "login_email";
    private static String OPEN_TAB_OWN_RANDOS = "open_tab_own_randos";
    private static String OPEN_TAB_STRANGER_RANDOS = "open_tab_stranger_randos";
    private static String OPEN_TAB_SETTINGS = "open_tab_settings";
    private static String SWITCH_CAMERA_TO_FRONT = "switch_camera_to_front";
    private static String SWITCH_CAMERA_TO_BACK = "switch_camera_to_back";
    private static String RATE_RANDO_UP = "rate_rando_up";
    private static String RATE_RANDO_NORMAL = "rate_rando_normal";
    private static String RATE_RANDO_DOWN = "rate_rando_down";

    //unwanted rando events
    private static String CLICK_UNWANTED_RANDO = "click_unwanted_rando";
    private static String DELETE_UNWANTED_RANDO_DIALOG = "delete_unwanted_rando_dialog";
    private static String CANCEL_UNWANTED_RANDO_DIALOG = "cancel_unwanted_rando_dialog";

    public static void logTakeRando(FirebaseAnalytics analytics) {
        analytics.logEvent(TAKE_RANDO, null);
    }

    public static void logUploadRando(FirebaseAnalytics analytics) {
        analytics.logEvent(UPLOAD_RANDO, null);
    }

    public static void logShareRando(FirebaseAnalytics analytics) {
        analytics.logEvent(SHARE_RANDO, null);
    }

    public static void logDeleteRando(FirebaseAnalytics analytics) {
        analytics.logEvent(DELETE_RANDO, null);
    }

    public static void logReportRando(FirebaseAnalytics analytics) {
        analytics.logEvent(REPORT_RANDO, null);
    }

    public static void logTapStrangerRando(FirebaseAnalytics analytics) {
        analytics.logEvent(TAP_STRANGER_RANDO, null);
    }

    public static void logTapOwnRando(FirebaseAnalytics analytics) {
        analytics.logEvent(TAP_OWN_RANDO, null);
    }

    public static void logForceSync(FirebaseAnalytics analytics) {
        analytics.logEvent(FORCE_SYNC, null);
    }

    public static void logLogout(FirebaseAnalytics analytics) {
        analytics.logEvent(LOGOUT, null);
    }

    public static void logLoginSkip(FirebaseAnalytics analytics) {
        analytics.logEvent(LOGIN_SKIP, null);
    }

    public static void logLoginEmail(FirebaseAnalytics analytics) {
        analytics.logEvent(LOGIN_EMAIL, null);
    }

    public static void logLoginGoogle(FirebaseAnalytics analytics) {
        analytics.logEvent(LOGIN_GOOGLE, null);
    }

    public static void logOpenTabOwnRandos(FirebaseAnalytics analytics) {
        analytics.logEvent(OPEN_TAB_OWN_RANDOS, null);
    }

    public static void logOpenTabStrangerRandos(FirebaseAnalytics analytics) {
        analytics.logEvent(OPEN_TAB_STRANGER_RANDOS, null);
    }

    public static void logOpenTabSettings(FirebaseAnalytics analytics) {
        analytics.logEvent(OPEN_TAB_SETTINGS, null);
    }

    public static void logClickUnwantedRando(FirebaseAnalytics analytics) {
        analytics.logEvent(CLICK_UNWANTED_RANDO, null);
    }

    public static void logDeleteUnwantedRandoDialog(FirebaseAnalytics analytics) {
        analytics.logEvent(DELETE_UNWANTED_RANDO_DIALOG, null);
    }

    public static void logCancelUnwantedRandoDialog(FirebaseAnalytics analytics) {
        analytics.logEvent(CANCEL_UNWANTED_RANDO_DIALOG, null);
    }

    public static void logSwitchCameraToFront(FirebaseAnalytics analytics) {
        analytics.logEvent(SWITCH_CAMERA_TO_FRONT, null);
    }

    public static void logSwitchCameraToBack(FirebaseAnalytics analytics) {
        analytics.logEvent(SWITCH_CAMERA_TO_BACK, null);
    }

    public static void logRateRandoGood(FirebaseAnalytics analytics) {
        analytics.logEvent(RATE_RANDO_UP, null);
    }

    public static void logRateRandoNormal(FirebaseAnalytics analytics) {
        analytics.logEvent(RATE_RANDO_NORMAL, null);
    }

    public static void logRateRandoBad(FirebaseAnalytics analytics) {
        analytics.logEvent(RATE_RANDO_DOWN, null);
    }

} public class BitmapUtil {

    private static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }

    public static Bitmap decodeSampledBitmap(String file, int reqWidth, int reqHeight) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(file, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(file, options);
    }
} public class FlipImageView extends AppCompatImageView {

    private Animation[] leftToRightAnimation;

    public FlipImageView(Context context) {
        super(context);
    }

    public FlipImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public FlipImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        leftToRightAnimation = AnimationFactory.flipAnimation(w, AnimationFactory.FlipDirection.LEFT_RIGHT, 150, null);
    }

    public void flipView(final int imageResource, final int backgroundResource, final OnAnimationEnd onAnimationEnd) {
        final Animation anim_out = leftToRightAnimation[0];
        final Animation anim_in = leftToRightAnimation[1];
        anim_out.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                //Do nothing
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
                //Do nothing
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                setImageResource(imageResource);
                if (backgroundResource > 0) {
                    setBackgroundResource(backgroundResource);
                }
                anim_in.setAnimationListener(new Animation.AnimationListener() {
                    @Override
                    public void onAnimationStart(Animation animation) {
                        //Do nothing
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                        //Do nothing
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        if (onAnimationEnd != null) {
                            onAnimationEnd.onEnd();
                        }
                    }
                });
                startAnimation(anim_in);
            }
        });
        startAnimation(anim_out);
    }

} public class RandoOnLongTapListener implements View.OnLongClickListener {
    private RandoListAdapter randoListAdapter;
    private RandoListAdapter.RandoViewHolder holder;
    private FirebaseAnalytics firebaseAnalytics;

    public RandoOnLongTapListener(RandoListAdapter randoListAdapter, RandoListAdapter.RandoViewHolder holder, FirebaseAnalytics firebaseAnalytics) {
        this.randoListAdapter = randoListAdapter;
        this.holder = holder;
        this.firebaseAnalytics = firebaseAnalytics;
    }

    @Override
    public boolean onLongClick(View v) {
        holder.recycleRatingMenu();
        if (holder.circleMenu == null && !holder.rando.isUnwanted()) {
            Resources res = v.getResources();
            RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(holder.imageSize, holder.imageSize);

            holder.circleMenu = new CircleMenu(v.getContext());
            holder.randoItemLayout.addView(holder.circleMenu, layoutParams);

            holder.circleMenu.setMainMenu(res.getColor(R.color.menu_button_color), R.drawable.ic_close_white_24dp, R.drawable.ic_close_white_24dp);
            if (!holder.rando.isToUpload()) {
                holder.circleMenu.addSubMenu(res.getColor(R.color.share_menu_button_color), R.drawable.ic_share_white_24dp);
            }

            holder.circleMenu.addSubMenu(res.getColor(R.color.delete_menu_button_color), R.drawable.ic_delete_white_24dp);
            if (holder.rando.status.equals(Rando.Status.IN)) {
                holder.circleMenu.addSubMenu(res.getColor(R.color.report_menu_button_color), R.drawable.ic_flag_white_24dp);
            }
            holder.circleMenu.setOnMenuSelectedListener(new OnMenuSelectedListener() {

                @Override
                public void onMenuSelected(int index) {
                    if (index == 0 && holder.rando.isToUpload()) {
                        deleteRando(holder);
                        return;
                    }
                    switch (index) {
                        case 0:
                            shareRando(holder);
                            break;
                        case 1:
                            deleteRando(holder);
                            break;
                        case 2:
                            reportRando(holder);
                            break;
                        default:
                            break;
                    }
                }
            }).setOnMenuStatusChangeListener(new OnMenuStatusChangeListener() {

                @Override
                public void onMenuOpened() {
                    //do nothing
                }

                @Override
                public void onMenuClosed() {
                    holder.recycleCircleMenu();
                    if (holder.ratingMenu == null) {
                        holder.image.setAlpha(1f);
                        holder.map.setAlpha(1f);
                    }
                }

            });
            holder.image.setAlpha(0.25f);
            holder.map.setAlpha(0.25f);
            holder.circleMenu.openMenu();
        }
        return true;
    }

    private void deleteRando(final RandoListAdapter.RandoViewHolder holder) {
        Analytics.logDeleteRando(firebaseAnalytics);
        if (NetworkUtil.isOnline(holder.randoItemLayout.getContext()) || holder.rando.isToUpload()) {
            AlertDialog.Builder builder = new AlertDialog.Builder(holder.randoItemLayout.getContext());
            builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    if (holder.rando.isToUpload()) {
                        RandoDAO.deleteRandoToUploadById(holder.randoItemLayout.getContext(), holder.rando.id);
                        randoListAdapter.changeCursor(RandoDAO.getCursor(holder.image.getContext(), randoListAdapter.isStranger()));
                        randoListAdapter.notifyItemRemoved(holder.position);
                        return;
                    }
                    try {
                        holder.showSpinner(true);
                        API.delete(holder.rando.randoId, holder.randoItemLayout.getContext(), new NetworkResultListener(holder.randoItemLayout.getContext()) {
                            @Override
                            public void onOk() {
                                randoListAdapter.changeCursor(RandoDAO.getCursor(context, randoListAdapter.isStranger()));
                                randoListAdapter.notifyItemRemoved(holder.position);

                                makeText(holder.randoItemLayout.getContext(), R.string.rando_deleted,
                                        Toast.LENGTH_LONG).show();
                                holder.showSpinner(false);
                            }

                            @Override
                            protected void onFail(Error error) {
                                makeText(holder.randoItemLayout.getContext(), R.string.error_unknown_err,
                                        Toast.LENGTH_LONG).show();
                                holder.showSpinner(false);
                            }
                        });
                    } catch (Exception e) {
                        makeText(holder.randoItemLayout.getContext(), R.string.error_unknown_err,
                                Toast.LENGTH_LONG).show();
                        holder.showSpinner(false);
                    }
                }
            });
            builder.setNegativeButton(R.string.cancel, null).setTitle(R.string.delete_rando).setMessage(R.string.delete_rando_confirm).create().show();
            return;
        } else {
            makeText(holder.randoItemLayout.getContext(), R.string.error_no_network,
                    Toast.LENGTH_LONG).show();
        }
    }

    private void reportRando(final RandoListAdapter.RandoViewHolder holder) {
        Analytics.logReportRando(firebaseAnalytics);
        if (NetworkUtil.isOnline(holder.randoItemLayout.getContext())) {
            AlertDialog.Builder builder = new AlertDialog.Builder(holder.randoItemLayout.getContext());
            builder.setPositiveButton(R.string.report, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    try {
                        holder.showSpinner(true);
                        API.report(holder.rando.randoId, holder.randoItemLayout.getContext(), new NetworkResultListener(holder.randoItemLayout.getContext()) {
                            @Override
                            public void onOk() {
                                RandoDAO.deleteRandoByRandoId(holder.randoItemLayout.getContext(), holder.rando.randoId);
                                randoListAdapter.swapCursor(RandoDAO.getCursor(holder.image.getContext(), randoListAdapter.isStranger()));
                                randoListAdapter.notifyItemRemoved(holder.position);

                                makeText(holder.randoItemLayout.getContext(), R.string.rando_reported,
                                        Toast.LENGTH_LONG).show();
                                holder.showSpinner(false);
                            }

                            @Override
                            protected void onFail(Error error) {
                                makeText(holder.randoItemLayout.getContext(), R.string.error_unknown_err,
                                        Toast.LENGTH_LONG).show();
                                holder.showSpinner(false);
                            }
                        });
                    } catch (Exception e) {
                        makeText(holder.randoItemLayout.getContext(), R.string.error_unknown_err,
                                Toast.LENGTH_LONG).show();
                        holder.showSpinner(false);
                    }
                }
            });
            builder.setNegativeButton(R.string.cancel, null).setTitle(R.string.report_rando).setMessage(R.string.report_rando_confirm).create().show();
            return;
        } else {
            makeText(holder.randoItemLayout.getContext(), R.string.error_no_network,
                    Toast.LENGTH_LONG).show();
        }
    }

    private void shareRando(final RandoListAdapter.RandoViewHolder holder) {
        Analytics.logShareRando(firebaseAnalytics);
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setType("text/plain");
        shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);

        // Add data to the intent, the receiving app will decide
        // what to do with it.
        shareIntent.putExtra(Intent.EXTRA_SUBJECT, holder.randoItemLayout.getContext().getResources().getString(R.string.share_subject));
        shareIntent.putExtra(Intent.EXTRA_TEXT, holder.randoItemLayout.getContext().getResources().getString(R.string.share_text) + " " + String.format(Constants.SHARE_URL, holder.rando.randoId));
        holder.randoItemLayout.getContext().startActivity(Intent.createChooser(shareIntent, "Share Rando using"));
    }
} public class RateButtonOnClickListener implements View.OnClickListener {
    private RandoListAdapter randoListAdapter;
    private RandoListAdapter.RandoViewHolder holder;
    private FirebaseAnalytics firebaseAnalytics;

    public RateButtonOnClickListener(RandoListAdapter randoListAdapter, RandoListAdapter.RandoViewHolder holder, FirebaseAnalytics firebaseAnalytics) {
        this.randoListAdapter = randoListAdapter;
        this.holder = holder;
        this.firebaseAnalytics = firebaseAnalytics;
    }

    @Override
    public void onClick(View v) {
        holder.recycleCircleMenu();
        if (holder.ratingMenu != null) {
            return;
        }
        holder.ratingMenu = new CircleMenu(v.getContext());
        Resources res = v.getResources();
        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(holder.imageSize, holder.imageSize);
        holder.randoItemLayout.addView(holder.ratingMenu, layoutParams);

        holder.ratingMenu.setMainMenu(res.getColor(R.color.menu_button_color), R.drawable.ic_close_white_24dp, R.drawable.ic_close_white_24dp)
                .addSubMenu(res.getColor(R.color.thumbs_down_button_background), R.drawable.ic_thumb_down_white_24dp)
                .addSubMenu(res.getColor(R.color.thumbs_up_button_background), R.drawable.ic_thumb_up_white_24dp)
                .setOnMenuSelectedListener(new OnMenuSelectedListener() {
                    @Override
                    public void onMenuSelected(int index) {
                        boolean isRatingChanged = false;
                        switch (index) {
                            case 0:
                                Analytics.logRateRandoBad(firebaseAnalytics);
                                isRatingChanged = 1 != holder.rando.rating;
                                if (isRatingChanged) {
                                    holder.rando.rating = 1;
                                    rateRando(holder, 1);
                                }
                                break;
                            case 1:
                                Analytics.logRateRandoGood(firebaseAnalytics);
                                isRatingChanged = 3 != holder.rando.rating;
                                if (isRatingChanged) {
                                    holder.rando.rating = 3;
                                    rateRando(holder, 3);
                                }
                                break;
                            default:
                                break;
                        }

                        randoListAdapter.setRatingIcon(holder, isRatingChanged);
                    }

                }).setOnMenuStatusChangeListener(new OnMenuStatusChangeListener() {

            @Override
            public void onMenuOpened() {
                //do nothing
            }

            @Override
            public void onMenuClosed() {
                holder.recycleRatingMenu();
                if (holder.circleMenu == null) {
                    holder.image.setAlpha(1f);
                    holder.map.setAlpha(1f);
                }
            }
        }).openMenu();
        holder.image.setAlpha(0.25f);
        holder.map.setAlpha(0.25f);
    }

    private void rateRando(final RandoListAdapter.RandoViewHolder holder, final int newRating) {
        API.rate(holder.rando.randoId, holder.randoItemLayout.getContext(), newRating, new NetworkResultListener(holder.randoItemLayout.getContext()) {
            @Override
            public void onOk() {
                randoListAdapter.changeCursor(RandoDAO.getCursor(holder.image.getContext(), randoListAdapter.isStranger()));
                randoListAdapter.notifyItemChanged(holder.position);
            }

            @Override
            public void onError(Error error) {
                makeText(holder.randoItemLayout.getContext(), "Error setting rating. Please check internet connection.", Toast.LENGTH_LONG).show();
            }

            @Override
            protected void onFail(Error error) {
                makeText(holder.randoItemLayout.getContext(), R.string.error_unknown_err,
                        Toast.LENGTH_LONG).show();
                holder.showSpinner(false);
            }

        });
    }

} public class RoundProgress extends View {

    private Paint paint = new Paint();
    private float startAngle2 = 1;
    private int sweepAngle = -90;
    private RectF oval = new RectF();

    private float in_rad;
    private int currentColor;
    private int currentBackgroundColor;

    private int[] colors = {Color.parseColor("#AA68e4a2"), Color.parseColor("#AAdbd663")};
    private int currentColorId = 0;

    private boolean stopAnimation = false;

    public RoundProgress(Context context, float radius) {
        super(context);
        paint.setStyle(Paint.Style.STROKE);
        paint.setFlags(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true);
        this.in_rad = radius == 0 ? 50 : radius - 8;
        currentColor = colors[currentColorId];
        currentBackgroundColor = Color.parseColor("#66dce0df");
        paint.setStrokeWidth((int) (getResources().getDimensionPixelSize(R.dimen.rando_padding_portrait_column_left) * 1.4));
        post(animator);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (stopAnimation) {
            stopAnimation = false;
            post(animator);

        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        oval.set(getWidth() / 2 - in_rad, getHeight() / 2 - in_rad, getWidth() / 2 + in_rad, getHeight() / 2 + in_rad);
        paint.setColor(currentBackgroundColor);
        canvas.drawArc(oval, startAngle2 + sweepAngle, 360 - startAngle2, false, paint);
        paint.setColor(currentColor);
        canvas.drawArc(oval, sweepAngle, startAngle2, false, paint);
    }

    @Override
    public void clearAnimation() {
        super.clearAnimation();
        stopAnimation = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopAnimation = true;
    }

    private final Runnable animator = new Runnable() {
        @Override
        public void run() {
            if (startAngle2 < 360) {
                startAngle2 += 0.5;
            } else {
                startAngle2 = 0;
                currentBackgroundColor = colors[currentColorId];
                currentColorId = (currentColorId + 1) % colors.length;
                currentColor = colors[currentColorId];
            }
            invalidate();
            if (!stopAnimation) {
                postDelayed(this, 50);
            }
        }
    };

} public class UnwantedRandoView extends RelativeLayout {

    private final Animation animation;
    private boolean stopAnimation = false;

    public UnwantedRandoView(Context context) {
        super(context);
        RoundedImageView roundedImageView = new RoundedImageView(context);
        roundedImageView.setImageResource(R.drawable.ic_boring);
        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        roundedImageView.setLayoutParams(layoutParams);
        roundedImageView.setOval(true);
        addView(roundedImageView);
        animation = AnimationUtils.loadAnimation(context, R.anim.show_hide_infinity);
        startAnimation(animation);
    }

    @Override
    protected void onDetachedFromWindow() {
        clearAnimation();
        stopAnimation = true;
        super.onDetachedFromWindow();
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (stopAnimation) {
            stopAnimation = false;
            startAnimation(animation);
        }
    }
}
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.webkit.URLUtil;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewSwitcher;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.ImageLoader;
import com.github.randoapp.R;
import com.github.randoapp.animation.AnimationFactory;
import com.github.randoapp.animation.AnimationListenerAdapter;
import com.github.randoapp.animation.AnimatorListenerAdapter;
import com.github.randoapp.api.API;
import com.github.randoapp.api.beans.Error;
import com.github.randoapp.api.listeners.NetworkResultListener;
import com.github.randoapp.db.RandoDAO;
import com.github.randoapp.db.RandoDBHelper;
import com.github.randoapp.db.model.Rando;
import com.github.randoapp.log.Log;
import com.github.randoapp.network.VolleySingleton;
import com.github.randoapp.preferences.Preferences;
import com.github.randoapp.util.Analytics;
import com.github.randoapp.util.BitmapUtil;
import com.github.randoapp.view.FlipImageView;
import com.github.randoapp.view.RandoOnLongTapListener;
import com.github.randoapp.view.RateButtonOnClickListener;
import com.github.randoapp.view.RoundProgress;
import com.github.randoapp.view.UnwantedRandoView;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.hitomi.cmlibrary.CircleMenu;
import com.makeramen.roundedimageview.RoundedImageView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import static android.widget.Toast.makeText;
import static com.android.volley.Request.Priority;
                        holder.rateButton.setImageResource(R.drawable.ic_thumb_down_white_24dp);
                        holder.rateButton.setBackgroundResource(R.drawable.round_button_red);
                    }
                    break;
                case 3:
                    holder.rateButton.setVisibility(View.VISIBLE);
                    if (doAnimation) {
                        holder.rateButton.flipView(R.drawable.ic_thumb_up_white_24dp, R.drawable.round_button_green, null);
                    } else {
                        holder.rateButton.setImageResource(R.drawable.ic_thumb_up_white_24dp);
                        holder.rateButton.setBackgroundResource(R.drawable.round_button_green);
                    }
                    break;
                default:
                    holder.rateButton.setVisibility(View.VISIBLE);
                    holder.rateButton.setImageResource(R.drawable.ic_thumb_up_white_24dp);
                    holder.rateButton.setBackgroundResource(R.drawable.round_button_grey);
                    break;
            }
        }
    }

    private void recycle(RandoViewHolder holder) {
        holder.animationInProgress = false;

        cancelRequests(holder);

        recycleViewSwitcher(holder.viewSwitcher);

        holder.image.setImageBitmap(null);
        holder.map.setImageBitmap(null);
        holder.isMap = false;

        holder.image.setAlpha(1f);
        holder.map.setAlpha(1f);
        holder.showSpinner(false);

        holder.recycleCircleMenu();
        holder.recycleRatingMenu();

        if (holder.unwantedRandoView != null) {
            holder.unwantedRandoView.clearAnimation();
            holder.randoItemLayout.removeView(holder.unwantedRandoView);
            holder.unwantedRandoView = null;
        }

        if (holder.uploadingProgress != null) {
            holder.uploadingProgress.clearAnimation();
            holder.randoItemLayout.removeView(holder.uploadingProgress);
            holder.uploadingProgress = null;
        }

        if (holder.landingImage != null) {
            holder.landingImage.clearAnimation();
            ((FrameLayout) (holder.map.getParent())).removeView(holder.landingImage);
            holder.landingImage = null;
        }

        holder.rando = null;
    }


    private void recycleViewSwitcher(ViewSwitcher viewSwitcher) {
        //disable animation for immediately and undetectable switching to zero child:
        viewSwitcher.clearAnimation();
        viewSwitcher.setInAnimation(null);
        viewSwitcher.setOutAnimation(null);
        viewSwitcher.setDisplayedChild(0);
    }

    private void cancelRequests(RandoViewHolder holder) {
        if (holder.randoContainer != null) {
            holder.randoContainer.cancelRequest();
            holder.randoContainer = null;
        }
        if (holder.mapContainer != null) {
            holder.mapContainer.cancelRequest();
            holder.mapContainer = null;
        }
    }

    private int getRandoImageSize(ViewGroup container) {
        return container.getWidth() - container.getContext().getResources().getDimensionPixelSize(R.dimen.rando_padding_portrait_column_left)
                - container.getContext().getResources().getDimensionPixelSize(R.dimen.rando_padding_portrait_column_right);
    }

    private void setAnimations(final RandoViewHolder holder) {
        final Animation[] leftToRightAnimation = AnimationFactory.flipAnimation(imageSize, AnimationFactory.FlipDirection.LEFT_RIGHT, 350, null);
        final Animation[] rightToLeftAnimation = AnimationFactory.flipAnimation(imageSize, AnimationFactory.FlipDirection.RIGHT_LEFT, 350, null);

        holder.viewSwitcher.setOutAnimation(leftToRightAnimation[0]);
        holder.viewSwitcher.setInAnimation(leftToRightAnimation[1]);

        Animation.AnimationListener outAnimationListener = new AnimationListenerAdapter() {
            @Override
            public void onAnimationStart(Animation animation) {
                holder.animationInProgress = true;
            }
        };
        leftToRightAnimation[0].setAnimationListener(outAnimationListener);
        leftToRightAnimation[1].setAnimationListener(new AnimationListenerAdapter() {
            @Override
            public void onAnimationEnd(Animation animation) {
                holder.viewSwitcher.setOutAnimation(rightToLeftAnimation[0]);
                holder.viewSwitcher.setInAnimation(rightToLeftAnimation[1]);
                holder.animationInProgress = false;
            }
        });

        rightToLeftAnimation[0].setAnimationListener(outAnimationListener);
        rightToLeftAnimation[1].setAnimationListener(new AnimationListenerAdapter() {
            @Override
            public void onAnimationEnd(Animation animation) {
                holder.viewSwitcher.setOutAnimation(leftToRightAnimation[0]);
                holder.viewSwitcher.setInAnimation(leftToRightAnimation[1]);
                holder.animationInProgress = false;
            }
        });
    }

    private void loadImages(final Context context, final RandoViewHolder holder, final Rando rando) {