public class VaporApp extends Application {

    private static String TAG = "VaporApp";

    @Inject
    Bus bus;
    @Inject
    NotificationManager notificationManager;
    @Inject
    RefCountManager refCountManager;

    VaporAppComponent vaporAppComponent;
    UiComponent uiComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        //Bad static initializers.
        SugarContext.init(this);
        Once.initialise(this);
        Hawk.init(this)
        .setEncryptionMethod(HawkBuilder.EncryptionMethod.NO_ENCRYPTION)
                .setStorage(HawkBuilder.newSqliteStorage(this))
                .setLogLevel(LogLevel.FULL)
                .build();


        vaporAppComponent = VaporAppComponent.Initializer.init(this);

        uiComponent = DaggerUiComponent.builder()
                .vaporAppComponent(vaporAppComponent)
                .build();

        vaporAppComponent.inject(this);

        registerBus();

        initStetho();

        if (BuildConfig.DEBUG) {
            //When debugging logs will go through the Android logger
            Timber.plant(new Timber.DebugTree());

            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectCustomSlowCalls()
            .detectDiskReads()
            .detectDiskWrites()
            .penaltyLog()
            .build());

            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
            .detectActivityLeaks()
            .penaltyLog()
            .build());
        }

        Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this));
    }

    private void registerBus() {
        bus.register(this);
    }

    private void initStetho() {
        Stetho.initialize(
                Stetho.newInitializerBuilder(this)
                        .enableDumpapp(
                                Stetho.defaultDumperPluginsProvider(this))
                        .enableWebKitInspector(
                                Stetho.defaultInspectorModulesProvider(this))
                        .build());
    }


    public VaporApp() {
    }

    public static UiComponent uiComponent(Context context) {
        return ((VaporApp) context.getApplicationContext()).uiComponent;
    }

    public static VaporAppComponent appComponent(Context context) {
        return ((VaporApp) context.getApplicationContext()).vaporAppComponent;
    }

    public static void recreateUiComponent(Context context) {
        ((VaporApp) context.getApplicationContext()).uiComponent = DaggerUiComponent.builder()
                .vaporAppComponent(((VaporApp) context.getApplicationContext()).vaporAppComponent)
                .build();
    }

    public static void recreateVaporComponent(Context context) {
        ((VaporApp) context.getApplicationContext()).vaporAppComponent =
                VaporAppComponent.Initializer.init((VaporApp)context.getApplicationContext());
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        Glide.get(this).clearMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        Glide.get(this).trimMemory(level);
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
        SugarContext.terminate();
        bus.unregister(this);

        for (Integer notificationId : refCountManager.getNotificationIds()) {
            notificationManager.cancel(notificationId);
            Timber.i("CLEAN UP from onTerminate with id=%s", notificationId);
        }
    }

    @Subscribe
    public void onLogoutEvent(LogoutEvent event) {
        // Clear the cache of the Http client before we create a new one.
        try {
            vaporAppComponent.client().getCache().evictAll();
        } catch (IOException e) {
            e.printStackTrace();
        }
        VaporApp.recreateVaporComponent(this);
        VaporApp.recreateUiComponent(this);
        Glide.get(VaporApp.this).clearMemory();
        Single.just(new Object()).subscribeOn(Schedulers.io()).subscribe(new Action1<Object>() {
            @Override
            public void call(Object o) {
                Glide.get(VaporApp.this).clearDiskCache();
            }
        });


        vaporAppComponent.inject(this);
        registerBus();


    }

    /*http://stackoverflow.com/questions/4028742/how-to-clear-a-notification-if-activity-crashes*/
    public class CrashHandler implements Thread.UncaughtExceptionHandler {

        private Thread.UncaughtExceptionHandler defaultUEH;
        private NotificationManager notificationManager;

        public CrashHandler(Context context) {
            this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
            notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        }

        public void uncaughtException(Thread t, Throwable e) {
            for (Integer notificationId : refCountManager.getNotificationIds()) {
                notificationManager.cancel(notificationId);
                Timber.i("CLEAN UP from uncaughtException with id=%s", notificationId);
            }
            notificationManager = null;
            defaultUEH.uncaughtException(t, e);
        }
    }
} public class TouchImageView extends ImageView {

    private static final String DEBUG = "DEBUG";

    //
    // SuperMin and SuperMax multipliers. Determine how much the image can be
    // zoomed below or above the zoom boundaries, before animating back to the
    // min/max zoom boundary.
    //
    private static final float SUPER_MIN_MULTIPLIER = .75f;
    private static final float SUPER_MAX_MULTIPLIER = 1.25f;

    //
    // Scale of image ranges from minScale to maxScale, where minScale == 1
    // when the image is stretched to fit view.
    //
    private float normalizedScale;

    //
    // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
    // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix
    // saved prior to the screen rotating.
    //
    private Matrix matrix, prevMatrix;

    private static enum State { NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM };
    private State state;

    private float minScale;
    private float maxScale;
    private float superMinScale;
    private float superMaxScale;
    private float[] m;

    private Context context;
    private Fling fling;

    private ScaleType mScaleType;

    private boolean imageRenderedAtLeastOnce;
    private boolean onDrawReady;

    private ZoomVariables delayedZoomVariables;

    //
    // Size of view and previous view size (ie before rotation)
    //
    private int viewWidth, viewHeight, prevViewWidth, prevViewHeight;

    //
    // Size of image when it is stretched to fit view. Before and After rotation.
    //
    private float matchViewWidth, matchViewHeight, prevMatchViewWidth, prevMatchViewHeight;

    private ScaleGestureDetector mScaleDetector;
    private GestureDetector mGestureDetector;
    private GestureDetector.OnDoubleTapListener doubleTapListener = null;
    private OnTouchListener userTouchListener = null;
    private OnTouchImageViewListener touchImageViewListener = null;

    public TouchImageView(Context context) {
        super(context);
        sharedConstructing(context);
    }

    public TouchImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        sharedConstructing(context);
    }

    public TouchImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        sharedConstructing(context);
    }

    private void sharedConstructing(Context context) {
        super.setClickable(true);
        this.context = context;
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
        mGestureDetector = new GestureDetector(context, new GestureListener());
        matrix = new Matrix();
        prevMatrix = new Matrix();
        m = new float[9];
        normalizedScale = 1;
        if (mScaleType == null) {
            mScaleType = ScaleType.FIT_CENTER;
        }
        minScale = 1;
        maxScale = 3;
        superMinScale = SUPER_MIN_MULTIPLIER * minScale;
        superMaxScale = SUPER_MAX_MULTIPLIER * maxScale;
        setImageMatrix(matrix);
        setScaleType(ScaleType.MATRIX);
        setState(State.NONE);
        onDrawReady = false;
        super.setOnTouchListener(new PrivateOnTouchListener());
    }

    @Override
    public void setOnTouchListener(View.OnTouchListener l) {
        userTouchListener = l;
    }

    public void setOnTouchImageViewListener(OnTouchImageViewListener l) {
        touchImageViewListener = l;
    }

    public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener l) {
        doubleTapListener = l;
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        savePreviousImageValues();
        fitImageToView();
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        savePreviousImageValues();
        fitImageToView();
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        savePreviousImageValues();
        fitImageToView();
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        savePreviousImageValues();
        fitImageToView();
    }

    @Override
    public void setScaleType(ScaleType type) {
        if (type == ScaleType.FIT_START || type == ScaleType.FIT_END) {
            throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
        }
        if (type == ScaleType.MATRIX) {
            super.setScaleType(ScaleType.MATRIX);

        } else {
            mScaleType = type;
            if (onDrawReady) {
                //
                // If the image is already rendered, scaleType has been called programmatically
                // and the TouchImageView should be updated with the new scaleType.
                //
                setZoom(this);
            }
        }
    }

    @Override
    public ScaleType getScaleType() {
        return mScaleType;
    }

    /**
     * Returns false if image is in initial, unzoomed state. False, otherwise.
     * @return true if image is zoomed
     */
    public boolean isZoomed() {
        return normalizedScale != 1;
    }

    /**
     * Return a Rect representing the zoomed image.
     * @return rect representing zoomed image
     */
    public RectF getZoomedRect() {
        if (mScaleType == ScaleType.FIT_XY) {
            throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY");
        }
        PointF topLeft = transformCoordTouchToBitmap(0, 0, true);
        PointF bottomRight = transformCoordTouchToBitmap(viewWidth, viewHeight, true);

        float w = getDrawable().getIntrinsicWidth();
        float h = getDrawable().getIntrinsicHeight();
        return new RectF(topLeft.x / w, topLeft.y / h, bottomRight.x / w, bottomRight.y / h);
    }

    /**
     * Save the current matrix and view dimensions
     * in the prevMatrix and prevView variables.
     */
    private void savePreviousImageValues() {
        if (matrix != null && viewHeight != 0 && viewWidth != 0) {
            matrix.getValues(m);
            prevMatrix.setValues(m);
            prevMatchViewHeight = matchViewHeight;
            prevMatchViewWidth = matchViewWidth;
            prevViewHeight = viewHeight;
            prevViewWidth = viewWidth;
        }
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable("instanceState", super.onSaveInstanceState());
        bundle.putFloat("saveScale", normalizedScale);
        bundle.putFloat("matchViewHeight", matchViewHeight);
        bundle.putFloat("matchViewWidth", matchViewWidth);
        bundle.putInt("viewWidth", viewWidth);
        bundle.putInt("viewHeight", viewHeight);
        matrix.getValues(m);
        bundle.putFloatArray("matrix", m);
        bundle.putBoolean("imageRendered", imageRenderedAtLeastOnce);
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            normalizedScale = bundle.getFloat("saveScale");
            m = bundle.getFloatArray("matrix");
            prevMatrix.setValues(m);
            prevMatchViewHeight = bundle.getFloat("matchViewHeight");
            prevMatchViewWidth = bundle.getFloat("matchViewWidth");
            prevViewHeight = bundle.getInt("viewHeight");
            prevViewWidth = bundle.getInt("viewWidth");
            imageRenderedAtLeastOnce = bundle.getBoolean("imageRendered");
            super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
            return;
        }

        super.onRestoreInstanceState(state);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        onDrawReady = true;
        imageRenderedAtLeastOnce = true;
        if (delayedZoomVariables != null) {
            setZoom(delayedZoomVariables.scale, delayedZoomVariables.focusX, delayedZoomVariables.focusY, delayedZoomVariables.scaleType);
            delayedZoomVariables = null;
        }
        super.onDraw(canvas);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        savePreviousImageValues();
    }

    /**
     * Get the max zoom multiplier.
     * @return max zoom multiplier.
     */
    public float getMaxZoom() {
        return maxScale;
    }

    /**
     * Set the max zoom multiplier. Default value: 3.
     * @param max max zoom multiplier.
     */
    public void setMaxZoom(float max) {
        maxScale = max;
        superMaxScale = SUPER_MAX_MULTIPLIER * maxScale;
    }

    /**
     * Get the min zoom multiplier.
     * @return min zoom multiplier.
     */
    public float getMinZoom() {
        return minScale;
    }

    /**
     * Get the current zoom. This is the zoom relative to the initial
     * scale, not the original resource.
     * @return current zoom multiplier.
     */
    public float getCurrentZoom() {
        return normalizedScale;
    }

    /**
     * Set the min zoom multiplier. Default value: 1.
     * @param min min zoom multiplier.
     */
    public void setMinZoom(float min) {
        minScale = min;
        superMinScale = SUPER_MIN_MULTIPLIER * minScale;
    }

    /**
     * Reset zoom and translation to initial state.
     */
    public void resetZoom() {
        normalizedScale = 1;
        fitImageToView();
    }

    /**
     * Set zoom to the specified scale. Image will be centered by default.
     * @param scale
     */
    public void setZoom(float scale) {
        setZoom(scale, 0.5f, 0.5f);
    }

    /**
     * Set zoom to the specified scale. Image will be centered around the point
     * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
     * as a fraction from the left and top of the view. For example, the top left
     * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
     * @param scale
     * @param focusX
     * @param focusY
     */
    public void setZoom(float scale, float focusX, float focusY) {
        setZoom(scale, focusX, focusY, mScaleType);
    }

    /**
     * Set zoom to the specified scale. Image will be centered around the point
     * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
     * as a fraction from the left and top of the view. For example, the top left
     * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
     * @param scale
     * @param focusX
     * @param focusY
     * @param scaleType
     */
    public void setZoom(float scale, float focusX, float focusY, ScaleType scaleType) {
        //
        // setZoom can be called before the image is on the screen, but at this point,
        // image and view sizes have not yet been calculated in onMeasure. Thus, we should
        // delay calling setZoom until the view has been measured.
        //
        if (!onDrawReady) {
            delayedZoomVariables = new ZoomVariables(scale, focusX, focusY, scaleType);
            return;
        }

        if (scaleType != mScaleType) {
            setScaleType(scaleType);
        }
        resetZoom();
        scaleImage(scale, viewWidth / 2, viewHeight / 2, true);
        matrix.getValues(m);
        m[Matrix.MTRANS_X] = -((focusX * getImageWidth()) - (viewWidth * 0.5f));
        m[Matrix.MTRANS_Y] = -((focusY * getImageHeight()) - (viewHeight * 0.5f));
        matrix.setValues(m);
        fixTrans();
        setImageMatrix(matrix);
    }

    public void setZoom(TouchImageView img) {
        PointF center = img.getScrollPosition();
        setZoom(img.getCurrentZoom(), center.x, center.y, img.getScaleType());
    }

    /**
     * Return the point at the center of the zoomed image. The PointF coordinates range
     * in value between 0 and 1 and the focus point is denoted as a fraction from the left
     * and top of the view. For example, the top left corner of the image would be (0, 0).
     * And the bottom right corner would be (1, 1).
     * @return PointF representing the scroll position of the zoomed image.
     */
    public PointF getScrollPosition() {
        Drawable drawable = getDrawable();
        if (drawable == null) {
            return null;
        }
        int drawableWidth = drawable.getIntrinsicWidth();
        int drawableHeight = drawable.getIntrinsicHeight();

        PointF point = transformCoordTouchToBitmap(viewWidth / 2, viewHeight / 2, true);
        point.x /= drawableWidth;
        point.y /= drawableHeight;
        return point;
    }

    /**
     * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the
     * left and top of the view. The focus points can range in value between 0 and 1.
     * @param focusX
     * @param focusY
     */
    public void setScrollPosition(float focusX, float focusY) {
        setZoom(normalizedScale, focusX, focusY);
    }

    /**
     * Performs boundary checking and fixes the image matrix if it
     * is out of bounds.
     */
    private void fixTrans() {
        matrix.getValues(m);
        float transX = m[Matrix.MTRANS_X];
        float transY = m[Matrix.MTRANS_Y];

        float fixTransX = getFixTrans(transX, viewWidth, getImageWidth());
        float fixTransY = getFixTrans(transY, viewHeight, getImageHeight());

        if (fixTransX != 0 || fixTransY != 0) {
            matrix.postTranslate(fixTransX, fixTransY);
        }
    }

    /**
     * When transitioning from zooming from focus to zoom from center (or vice versa)
     * the image can become unaligned within the view. This is apparent when zooming
     * quickly. When the content size is less than the view size, the content will often
     * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and
     * then makes sure the image is centered correctly within the view.
     */
    private void fixScaleTrans() {
        fixTrans();
        matrix.getValues(m);
        if (getImageWidth() < viewWidth) {
            m[Matrix.MTRANS_X] = (viewWidth - getImageWidth()) / 2;
        }

        if (getImageHeight() < viewHeight) {
            m[Matrix.MTRANS_Y] = (viewHeight - getImageHeight()) / 2;
        }
        matrix.setValues(m);
    }

    private float getFixTrans(float trans, float viewSize, float contentSize) {
        float minTrans, maxTrans;

        if (contentSize <= viewSize) {
            minTrans = 0;
            maxTrans = viewSize - contentSize;

        } else {
            minTrans = viewSize - contentSize;
            maxTrans = 0;
        }

        if (trans < minTrans)
            return -trans + minTrans;
        if (trans > maxTrans)
            return -trans + maxTrans;
        return 0;
    }

    private float getFixDragTrans(float delta, float viewSize, float contentSize) {
        if (contentSize <= viewSize) {
            return 0;
        }
        return delta;
    }

    private float getImageWidth() {
        return matchViewWidth * normalizedScale;
    }

    private float getImageHeight() {
        return matchViewHeight * normalizedScale;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Drawable drawable = getDrawable();
        if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
            setMeasuredDimension(0, 0);
            return;
        }

        int drawableWidth = drawable.getIntrinsicWidth();
        int drawableHeight = drawable.getIntrinsicHeight();
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        viewWidth = setViewSize(widthMode, widthSize, drawableWidth);
        viewHeight = setViewSize(heightMode, heightSize, drawableHeight);

        //
        // Set view dimensions
        //
        setMeasuredDimension(viewWidth, viewHeight);

        //
        // Fit content within view
        //
        fitImageToView();
    }

    /**
     * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise,
     * it is made to fit the screen according to the dimensions of the previous image matrix. This
     * allows the image to maintain its zoom after rotation.
     */
    private void fitImageToView() {
        Drawable drawable = getDrawable();
        if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
            return;
        }
        if (matrix == null || prevMatrix == null) {
            return;
        }

        int drawableWidth = drawable.getIntrinsicWidth();
        int drawableHeight = drawable.getIntrinsicHeight();

        //
        // Scale image for view
        //
        float scaleX = (float) viewWidth / drawableWidth;
        float scaleY = (float) viewHeight / drawableHeight;

        switch (mScaleType) {
            case CENTER:
                scaleX = scaleY = 1;
                break;

            case CENTER_CROP:
                scaleX = scaleY = Math.max(scaleX, scaleY);
                break;

            case CENTER_INSIDE:
                scaleX = scaleY = Math.min(1, Math.min(scaleX, scaleY));

            case FIT_CENTER:
                scaleX = scaleY = Math.min(scaleX, scaleY);
                break;

            case FIT_XY:
                break;

            default:
                //
                // FIT_START and FIT_END not supported
                //
                throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");

        }

        //
        // Center the image
        //
        float redundantXSpace = viewWidth - (scaleX * drawableWidth);
        float redundantYSpace = viewHeight - (scaleY * drawableHeight);
        matchViewWidth = viewWidth - redundantXSpace;
        matchViewHeight = viewHeight - redundantYSpace;
        if (!isZoomed() && !imageRenderedAtLeastOnce) {
            //
            // Stretch and center image to fit view
            //
            matrix.setScale(scaleX, scaleY);
            matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2);
            normalizedScale = 1;

        } else {
            //
            // These values should never be 0 or we will set viewWidth and viewHeight
            // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues
            // to set them equal to the current values.
            //
            if (prevMatchViewWidth == 0 || prevMatchViewHeight == 0) {
                savePreviousImageValues();
            }

            prevMatrix.getValues(m);

            //
            // Rescale Matrix after rotation
            //
            m[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * normalizedScale;
            m[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * normalizedScale;

            //
            // TransX and TransY from previous matrix
            //
            float transX = m[Matrix.MTRANS_X];
            float transY = m[Matrix.MTRANS_Y];

            //
            // Width
            //
            float prevActualWidth = prevMatchViewWidth * normalizedScale;
            float actualWidth = getImageWidth();
            translateMatrixAfterRotate(Matrix.MTRANS_X, transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth);

            //
            // Height
            //
            float prevActualHeight = prevMatchViewHeight * normalizedScale;
            float actualHeight = getImageHeight();
            translateMatrixAfterRotate(Matrix.MTRANS_Y, transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight);

            //
            // Set the matrix to the adjusted scale and translate values.
            //
            matrix.setValues(m);
        }
        fixTrans();
        setImageMatrix(matrix);
    }

    /**
     * Set view dimensions based on layout params
     *
     * @param mode
     * @param size
     * @param drawableWidth
     * @return
     */
    private int setViewSize(int mode, int size, int drawableWidth) {
        int viewSize;
        switch (mode) {
            case MeasureSpec.EXACTLY:
                viewSize = size;
                break;

            case MeasureSpec.AT_MOST:
                viewSize = Math.min(drawableWidth, size);
                break;

            case MeasureSpec.UNSPECIFIED:
                viewSize = drawableWidth;
                break;

            default:
                viewSize = size;
                break;
        }
        return viewSize;
    }

    /**
     * After rotating, the matrix needs to be translated. This function finds the area of image
     * which was previously centered and adjusts translations so that is again the center, post-rotation.
     *
     * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y
     * @param trans the value of trans in that axis before the rotation
     * @param prevImageSize the width/height of the image before the rotation
     * @param imageSize width/height of the image after rotation
     * @param prevViewSize width/height of view before rotation
     * @param viewSize width/height of view after rotation
     * @param drawableSize width/height of drawable
     */
    private void translateMatrixAfterRotate(int axis, float trans, float prevImageSize, float imageSize, int prevViewSize, int viewSize, int drawableSize) {
        if (imageSize < viewSize) {
            //
            // The width/height of image is less than the view's width/height. Center it.
            //
            m[axis] = (viewSize - (drawableSize * m[Matrix.MSCALE_X])) * 0.5f;

        } else if (trans > 0) {
            //
            // The image is larger than the view, but was not before rotation. Center it.
            //
            m[axis] = -((imageSize - viewSize) * 0.5f);

        } else {
            //
            // Find the area of the image which was previously centered in the view. Determine its distance
            // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage
            // to calculate the trans in the new view width/height.
            //
            float percentage = (Math.abs(trans) + (0.5f * prevViewSize)) / prevImageSize;
            m[axis] = -((percentage * imageSize) - (viewSize * 0.5f));
        }
    }

    private void setState(State state) {
        this.state = state;
    }

    public boolean canScrollHorizontallyFroyo(int direction) {
        return canScrollHorizontally(direction);
    }

    @Override
    public boolean canScrollHorizontally(int direction) {
        matrix.getValues(m);
        float x = m[Matrix.MTRANS_X];

        if (getImageWidth() < viewWidth) {
            return false;

        } else if (x >= -1 && direction < 0) {
            return false;

        } else if (Math.abs(x) + viewWidth + 1 >= getImageWidth() && direction > 0) {
            return false;
        }

        return true;
    }

    /**
     * Gesture Listener detects a single click or long click and passes that on
     * to the view's listener.
     * @author Ortiz
     *
     */
    private class GestureListener extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e)
        {
            if(doubleTapListener != null) {
                return doubleTapListener.onSingleTapConfirmed(e);
            }
            return performClick();
        }

        @Override
        public void onLongPress(MotionEvent e)
        {
            performLongClick();
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
        {
            if (fling != null) {
                //
                // If a previous fling is still active, it should be cancelled so that two flings
                // are not run simultaenously.
                //
                fling.cancelFling();
            }
            fling = new Fling((int) velocityX, (int) velocityY);
            compatPostOnAnimation(fling);
            return super.onFling(e1, e2, velocityX, velocityY);
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            boolean consumed = false;
            if(doubleTapListener != null) {
                consumed = doubleTapListener.onDoubleTap(e);
            }
            if (state == State.NONE) {
                float targetZoom = (normalizedScale == minScale) ? maxScale : minScale;
                DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false);
                compatPostOnAnimation(doubleTap);
                consumed = true;
            }
            return consumed;
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            if(doubleTapListener != null) {
                return doubleTapListener.onDoubleTapEvent(e);
            }
            return false;
        }
    }

    public interface OnTouchImageViewListener {
        public void onMove();
    }

    /**
     * Responsible for all touch events. Handles the heavy lifting of drag and also sends
     * touch events to Scale Detector and Gesture Detector.
     * @author Ortiz
     *
     */
    private class PrivateOnTouchListener implements OnTouchListener {

        //
        // Remember last point position for dragging
        //
        private PointF last = new PointF();

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            mScaleDetector.onTouchEvent(event);
            mGestureDetector.onTouchEvent(event);
            PointF curr = new PointF(event.getX(), event.getY());

            if (state == State.NONE || state == State.DRAG || state == State.FLING) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        last.set(curr);
                        if (fling != null)
                            fling.cancelFling();
                        setState(State.DRAG);
                        break;

                    case MotionEvent.ACTION_MOVE:
                        if (state == State.DRAG) {
                            float deltaX = curr.x - last.x;
                            float deltaY = curr.y - last.y;
                            float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth());
                            float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight());
                            matrix.postTranslate(fixTransX, fixTransY);
                            fixTrans();
                            last.set(curr.x, curr.y);
                        }
                        break;

                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_POINTER_UP:
                        setState(State.NONE);
                        break;
                }
            }

            setImageMatrix(matrix);

            //
            // User-defined OnTouchListener
            //
            if(userTouchListener != null) {
                userTouchListener.onTouch(v, event);
            }

            //
            // OnTouchImageViewListener is set: TouchImageView dragged by user.
            //
            if (touchImageViewListener != null) {
                touchImageViewListener.onMove();
            }

            //
            // indicate event was handled
            //
            return true;
        }
    }

    /**
     * ScaleListener detects user two finger scaling and scales image.
     * @author Ortiz
     *
     */
    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            setState(State.ZOOM);
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true);

            //
            // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user.
            //
            if (touchImageViewListener != null) {
                touchImageViewListener.onMove();
            }
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            super.onScaleEnd(detector);
            setState(State.NONE);
            boolean animateToZoomBoundary = false;
            float targetZoom = normalizedScale;
            if (normalizedScale > maxScale) {
                targetZoom = maxScale;
                animateToZoomBoundary = true;

            } else if (normalizedScale < minScale) {
                targetZoom = minScale;
                animateToZoomBoundary = true;
            }

            if (animateToZoomBoundary) {
                DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true);
                compatPostOnAnimation(doubleTap);
            }
        }
    }

    private void scaleImage(double deltaScale, float focusX, float focusY, boolean stretchImageToSuper) {

        float lowerScale, upperScale;
        if (stretchImageToSuper) {
            lowerScale = superMinScale;
            upperScale = superMaxScale;

        } else {
            lowerScale = minScale;
            upperScale = maxScale;
        }

        float origScale = normalizedScale;
        normalizedScale *= deltaScale;
        if (normalizedScale > upperScale) {
            normalizedScale = upperScale;
            deltaScale = upperScale / origScale;
        } else if (normalizedScale < lowerScale) {
            normalizedScale = lowerScale;
            deltaScale = lowerScale / origScale;
        }

        matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY);
        fixScaleTrans();
    }

    /**
     * DoubleTapZoom calls a series of runnables which apply
     * an animated zoom in/out graphic to the image.
     * @author Ortiz
     *
     */
    private class DoubleTapZoom implements Runnable {

        private long startTime;
        private static final float ZOOM_TIME = 500;
        private float startZoom, targetZoom;
        private float bitmapX, bitmapY;
        private boolean stretchImageToSuper;
        private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
        private PointF startTouch;
        private PointF endTouch;

        DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) {
            setState(State.ANIMATE_ZOOM);
            startTime = System.currentTimeMillis();
            this.startZoom = normalizedScale;
            this.targetZoom = targetZoom;
            this.stretchImageToSuper = stretchImageToSuper;
            PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false);
            this.bitmapX = bitmapPoint.x;
            this.bitmapY = bitmapPoint.y;

            //
            // Used for translating image during scaling
            //
            startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY);
            endTouch = new PointF(viewWidth / 2, viewHeight / 2);
        }

        @Override
        public void run() {
            float t = interpolate();
            double deltaScale = calculateDeltaScale(t);
            scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper);
            translateImageToCenterTouchPosition(t);
            fixScaleTrans();
            setImageMatrix(matrix);

            //
            // OnTouchImageViewListener is set: double tap runnable updates listener
            // with every frame.
            //
            if (touchImageViewListener != null) {
                touchImageViewListener.onMove();
            }

            if (t < 1f) {
                //
                // We haven't finished zooming
                //
                compatPostOnAnimation(this);

            } else {
                //
                // Finished zooming
                //
                setState(State.NONE);
            }
        }

        /**
         * Interpolate between where the image should start and end in order to translate
         * the image so that the point that is touched is what ends up centered at the end
         * of the zoom.
         * @param t
         */
        private void translateImageToCenterTouchPosition(float t) {
            float targetX = startTouch.x + t * (endTouch.x - startTouch.x);
            float targetY = startTouch.y + t * (endTouch.y - startTouch.y);
            PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY);
            matrix.postTranslate(targetX - curr.x, targetY - curr.y);
        }

        /**
         * Use interpolator to get t
         * @return
         */
        private float interpolate() {
            long currTime = System.currentTimeMillis();
            float elapsed = (currTime - startTime) / ZOOM_TIME;
            elapsed = Math.min(1f, elapsed);
            return interpolator.getInterpolation(elapsed);
        }

        /**
         * Interpolate the current targeted zoom and get the delta
         * from the current zoom.
         * @param t
         * @return
         */
        private double calculateDeltaScale(float t) {
            double zoom = startZoom + t * (targetZoom - startZoom);
            return zoom / normalizedScale;
        }
    }

    /**
     * This function will transform the coordinates in the touch event to the coordinate
     * system of the drawable that the imageview contain
     * @param x x-coordinate of touch event
     * @param y y-coordinate of touch event
     * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
     * 			to the bounds of the bitmap size.
     * @return Coordinates of the point touched, in the coordinate system of the original drawable.
     */
    private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) {
        matrix.getValues(m);
        float origW = getDrawable().getIntrinsicWidth();
        float origH = getDrawable().getIntrinsicHeight();
        float transX = m[Matrix.MTRANS_X];
        float transY = m[Matrix.MTRANS_Y];
        float finalX = ((x - transX) * origW) / getImageWidth();
        float finalY = ((y - transY) * origH) / getImageHeight();

        if (clipToBitmap) {
            finalX = Math.min(Math.max(finalX, 0), origW);
            finalY = Math.min(Math.max(finalY, 0), origH);
        }

        return new PointF(finalX , finalY);
    }

    /**
     * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
     * drawable's coordinate system to the view's coordinate system.
     * @param bx x-coordinate in original bitmap coordinate system
     * @param by y-coordinate in original bitmap coordinate system
     * @return Coordinates of the point in the view's coordinate system.
     */
    private PointF transformCoordBitmapToTouch(float bx, float by) {
        matrix.getValues(m);
        float origW = getDrawable().getIntrinsicWidth();
        float origH = getDrawable().getIntrinsicHeight();
        float px = bx / origW;
        float py = by / origH;
        float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px;
        float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py;
        return new PointF(finalX , finalY);
    }

    /**
     * Fling launches sequential runnables which apply
     * the fling graphic to the image. The values for the translation
     * are interpolated by the Scroller.
     * @author Ortiz
     *
     */
    private class Fling implements Runnable {

        CompatScroller scroller;
        int currX, currY;

        Fling(int velocityX, int velocityY) {
            setState(State.FLING);
            scroller = new CompatScroller(context);
            matrix.getValues(m);

            int startX = (int) m[Matrix.MTRANS_X];
            int startY = (int) m[Matrix.MTRANS_Y];
            int minX, maxX, minY, maxY;

            if (getImageWidth() > viewWidth) {
                minX = viewWidth - (int) getImageWidth();
                maxX = 0;

            } else {
                minX = maxX = startX;
            }

            if (getImageHeight() > viewHeight) {
                minY = viewHeight - (int) getImageHeight();
                maxY = 0;

            } else {
                minY = maxY = startY;
            }

            scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX,
                    maxX, minY, maxY);
            currX = startX;
            currY = startY;
        }

        public void cancelFling() {
            if (scroller != null) {
                setState(State.NONE);
                scroller.forceFinished(true);
            }
        }

        @Override
        public void run() {

            //
            // OnTouchImageViewListener is set: TouchImageView listener has been flung by user.
            // Listener runnable updated with each frame of fling animation.
            //
            if (touchImageViewListener != null) {
                touchImageViewListener.onMove();
            }

            if (scroller.isFinished()) {
                scroller = null;
                return;
            }

            if (scroller.computeScrollOffset()) {
                int newX = scroller.getCurrX();
                int newY = scroller.getCurrY();
                int transX = newX - currX;
                int transY = newY - currY;
                currX = newX;
                currY = newY;
                matrix.postTranslate(transX, transY);
                fixTrans();
                setImageMatrix(matrix);
                compatPostOnAnimation(this);
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    private class CompatScroller {
        Scroller scroller;
        OverScroller overScroller;
        boolean isPreGingerbread;

        public CompatScroller(Context context) {
            if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
                isPreGingerbread = true;
                scroller = new Scroller(context);

            } else {
                isPreGingerbread = false;
                overScroller = new OverScroller(context);
            }
        }

        public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
            if (isPreGingerbread) {
                scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
            } else {
                overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
            }
        }

        public void forceFinished(boolean finished) {
            if (isPreGingerbread) {
                scroller.forceFinished(finished);
            } else {
                overScroller.forceFinished(finished);
            }
        }

        public boolean isFinished() {
            if (isPreGingerbread) {
                return scroller.isFinished();
            } else {
                return overScroller.isFinished();
            }
        }

        public boolean computeScrollOffset() {
            if (isPreGingerbread) {
                return scroller.computeScrollOffset();
            } else {
                overScroller.computeScrollOffset();
                return overScroller.computeScrollOffset();
            }
        }

        public int getCurrX() {
            if (isPreGingerbread) {
                return scroller.getCurrX();
            } else {
                return overScroller.getCurrX();
            }
        }

        public int getCurrY() {
            if (isPreGingerbread) {
                return scroller.getCurrY();
            } else {
                return overScroller.getCurrY();
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void compatPostOnAnimation(Runnable runnable) {
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
            postOnAnimation(runnable);

        } else {
            postDelayed(runnable, 1000/60);
        }
    }

    private class ZoomVariables {
        public float scale;
        public float focusX;
        public float focusY;
        public ScaleType scaleType;

        public ZoomVariables(float scale, float focusX, float focusY, ScaleType scaleType) {
            this.scale = scale;
            this.focusX = focusX;
            this.focusY = focusY;
            this.scaleType = scaleType;
        }
    }

    private void printMatrixInfo() {
        float[] n = new float[9];
        matrix.getValues(n);
        Log.d(DEBUG, "Scale: " + n[Matrix.MSCALE_X] + " TransX: " + n[Matrix.MTRANS_X] + " TransY: " + n[Matrix.MTRANS_Y]);
    }
} public class DeleteEvent extends DataSetUpdateEvent {
    public DeleteEvent(CloudAppItem item) {
        super(item);
    }
} public class RenameEvent extends DataSetUpdateEvent {
    public RenameEvent(CloudAppItem item) {
        super(item);
    }
} public class DataManager {

    private final CloudAppService cloudAppService;
    private final Bus bus;
    private final UserManager userManager;
    private final int SERVER_ITEM_LIMIT = 40;
    private final int MAX_ITEM_LIMIT = 1500;
    private final OkHttpClient client;
    private boolean isSyncingAllItems;

    public final static String SYNC_ALL_ITEMS = DataManager.class.getPackage().getName() + ".SYNC_ALL_ITEMS";

    public DataManager(CloudAppService cloudAppService, UserManager userManager, Bus bus, OkHttpClient client) {
        this.cloudAppService = cloudAppService;
        this.bus = bus;
        this.userManager = userManager;
        this.client = client;
        bus.register(this);
    }

    public void syncAllItems(final boolean notify) {
        if (!isSyncingAllItems) {
            cloudAppService.getAccountStats()
                    .flatMap(new Func1<AccountStatsModel, Observable<List<CloudAppItem>>>() {
                        @Override
                        public Observable<List<CloudAppItem>> call(final AccountStatsModel accountStatsModel) {
                            return Observable.create(new Observable.OnSubscribe<Integer>() {
                                @Override
                                public void call(Subscriber<? super Integer> subscriber) {
                                    if (!subscriber.isUnsubscribed()) {
                                        for (int i = 1; i <= Math.ceil((double) accountStatsModel.getItems() / (double) SERVER_ITEM_LIMIT) + 1
                                                && i < Math.ceil(MAX_ITEM_LIMIT / SERVER_ITEM_LIMIT); i++) {
                                            subscriber.onNext(i);
                                        }
                                        subscriber.onCompleted();
                                    }
                                }
                            })
                                    .subscribeOn(Schedulers.io())
                                    .flatMap(new Func1<Integer, Observable<List<CloudAppItem>>>() {
                                        @Override
                                        public Observable<List<CloudAppItem>> call(Integer integer) {
                                            return getListFromServer(makeListParams(integer, ItemType.ALL, false, SERVER_ITEM_LIMIT));
                                        }
                                    }).subscribeOn(Schedulers.io())
                                    .flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                                        @Override
                                        public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                                            SugarRecord.updateInTx(cloudAppItems);
                                            return Observable.just(cloudAppItems);
                                        }
                                    });

                        }
                    })
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .doOnTerminate(new Action0() {
                        @Override
                        public void call() {
                            isSyncingAllItems = false;
                            if (notify) {
                                bus.post(new DatabaseUpdateEvent());
                            }
                        }
                    })
                    .doOnSubscribe(new Action0() {
                        @Override
                        public void call() {
                            isSyncingAllItems = true;
                        }
                    })
                    .subscribe(new Action1<List<CloudAppItem>>() {
                        @Override
                        public void call(List<CloudAppItem> cloudAppItems) {
                            Timber.i("Synced all items. Size: %s", cloudAppItems.size());
                        }
                    }, new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            Timber.e(throwable, "Error syncing all items");
                        }
                    });

            getTrashItems(ItemType.ALL, true, new DataCursor())
                    .subscribe(new Action1<List<CloudAppItem>>() {
                        @Override
                        public void call(List<CloudAppItem> cloudAppItems) {
                            Timber.i("Synced all deleted items. Size: %s", cloudAppItems.size());
                        }
                    }, new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            Timber.e(throwable, "Error syncing all deleted items");
                        }
                    });
        }
    }

    @NonNull
    private Func1<List<CloudAppItem>, Observable<CloudAppItem>> reduceList() {
        return new Func1<List<CloudAppItem>, Observable<CloudAppItem>>() {
            @Override
            public Observable<CloudAppItem> call(List<CloudAppItem> cloudAppItems) {
                return Observable.from(cloudAppItems);
            }
        };
    }

    @NonNull
    private Func1<CloudAppItem, CloudAppItem> saveToDb() {
        return new Func1<CloudAppItem, CloudAppItem>() {
            @Override
            public CloudAppItem call(CloudAppItem cloudAppItem) {
                SugarRecord.updateInTx(cloudAppItem);
                Timber.i("Inserting or update item: %s", cloudAppItem.getItemId());
                return cloudAppItem;
            }
        };
    }

    private void deleteLocalItem(CloudAppItem cloudAppItem) {
        SugarRecord.delete(cloudAppItem);
        Timber.i("Deleting item: %s", cloudAppItem.getItemId());
    }

    public void purgeDeletedItems() {
        long expired = DateTime.now().minusDays(7).getMillis();
        Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                "DELETE FROM CLOUD_APP_ITEM WHERE deleted_at <> -1 AND deleted_at < ?",
                String.valueOf(expired)))
                .subscribe(new Action1<List<CloudAppItem>>() {
                    @Override
                    public void call(List<CloudAppItem> cloudAppItems) {
                        Timber.i("Deleting items: %s", cloudAppItems.size());
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        Timber.e(throwable, "Error deleting stale items.");
                    }
                });
    }

    public Observable<CloudAppItem> deleteCloudItem(final CloudAppItem cloudAppItem) {
        return cloudAppService.deleteItem(String.valueOf(cloudAppItem.getItemId()))
                .map(convertItemModel())
                .doOnNext(new Action1<CloudAppItem>() {
                    @Override
                    public void call(CloudAppItem item) {
                        deleteLocalItem(cloudAppItem);
                    }
                });
    }

    public Observable<CloudAppItem> renameCloudItem(final CloudAppItem cloudAppItem, String newName) {
        CloudAppJsonItem jsonItem = new CloudAppJsonItem();
        CloudAppJsonItem.Item item = jsonItem.item = new CloudAppJsonItem.Item();
        item.name = newName;

        return cloudAppService.renameItem(String.valueOf(cloudAppItem.getItemId()), jsonItem)
                .map(convertItemModel())
                .map(saveToDb());
    }

    public Observable<CloudAppItem> bookmarkItem(final String name, String url) {
        CloudAppJsonItem jsonItem = new CloudAppJsonItem();
        CloudAppJsonItem.Item item = jsonItem.item = new CloudAppJsonItem.Item();
        item.name = name;
        item.redirectUrl = url;

        return cloudAppService.bookmarkLink(jsonItem)
                .map(convertItemModel())
                .map(saveToDb())
                .retryWhen(new RxUtils.RetryWithDelay(2, 2000));
    }

    public Observable<CloudAppItem> upload(final CloudAppRequestBody requestBody) {
        return cloudAppService.newUpload(makeQueryMap(requestBody.getFileName(), String.valueOf(requestBody.contentLength())))
                .flatMap(new Func1<UploadModel, Observable<CloudAppItem>>() {
                    @Override
                    public Observable<CloudAppItem> call(final UploadModel uploadModel) {
                        if (uploadModel.getParams() == null) {
                            return Observable.error(new UploadLimitException("Daily upload limit reached"));
                        }
                        if (requestBody.contentLength() > uploadModel.getMax_upload_size()) {
                            return Observable.error(new FileToLargeException("File too large for you current plan"));
                        }
                        return cloudAppService.uploadFile(makeMultipartParams(uploadModel, requestBody))
                                .map(convertItemModel())
                                .map(saveToDb())
                                .retryWhen(new RxUtils.RetryWithDelay(4, 2000));

                    }
                });
    }

    private Map<String, String> makeQueryMap(String name, String size) {
        Map<String, String> params = new LinkedHashMap<>();
        params.put("name", name);
        params.put("file_size", size);
        return params;
    }

    private Observable<List<CloudAppItem>> getAllItems(final ItemType type, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        if (type == ItemType.ALL) {
            return Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE deleted_at = -1 ORDER BY ITEM_ID DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"),
                            String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class, query,
                            String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        }
        return Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
            @Override
            public Observable<List<CloudAppItem>> call() {
                String query = "SELECT * FROM CLOUD_APP_ITEM WHERE ITEM_TYPE = ? AND deleted_at = -1 ORDER BY ITEM_ID DESC LIMIT ? OFFSET ?";
                Timber.d(query.replace("?", "%s"),
                        type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class, query,
                                type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
            }
        });
    }

    public Observable<List<CloudAppItem>> getPopularItems(final ItemType type, boolean refresh, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        final Observable<List<CloudAppItem>> observable;

        if (type == ItemType.ALL) {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE deleted_at = -1 ORDER BY VIEW_COUNTER DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        } else {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE ITEM_TYPE = ? AND deleted_at = -1 ORDER BY VIEW_COUNTER DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        }
        if (refresh) {
            return refreshPage(1, false)
                    .flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                    return observable;
                }
            });
        }
        return observable.doOnNext(new Action1<List<CloudAppItem>>() {
            @Override
            public void call(List<CloudAppItem> cloudAppItems) {
                cursor.setOffset(cursor.getOffset() + cloudAppItems.size());
            }
        });
    }

    public Observable<List<CloudAppItem>> getFavoriteItems(final ItemType type, boolean refresh, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        final Observable<List<CloudAppItem>> observable;

        if (type == ItemType.ALL) {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE FAVORITE = 1 AND deleted_at = -1 ORDER BY ITEM_ID DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }
            });
        } else {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE ITEM_TYPE = ? AND FAVORITE = 1 AND deleted_at = -1 ORDER BY ITEM_ID, VIEW_COUNTER DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        }
        if (refresh) {
            return refreshPage(1, false).flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                    return observable;
                }
            });
        }
        return observable.doOnNext(new Action1<List<CloudAppItem>>() {
            @Override
            public void call(List<CloudAppItem> cloudAppItems) {
                cursor.setOffset(cursor.getOffset() + cloudAppItems.size());
            }
        });
    }

    public Observable<List<CloudAppItem>> getTrashItems(final ItemType type, boolean refresh, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        final Observable<List<CloudAppItem>> observable;
        if (type == ItemType.ALL) {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE DELETED_AT <> -1 ORDER BY DELETED_AT DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return  Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }
            });
        } else {
            observable = Observable.defer(new Func0<Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call() {
                    String query = "SELECT * FROM CLOUD_APP_ITEM WHERE ITEM_TYPE = ? AND DELETED_AT <> -1 ORDER BY DELETED_AT DESC LIMIT ? OFFSET ?";
                    Timber.d(query.replace("?", "%s"), type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset()));
                    return Observable.just(SugarRecord.findWithQuery(CloudAppItem.class,
                            query, type.toString().toLowerCase(), String.valueOf(cursor.getLimit()), String.valueOf(cursor.getOffset())));
                }});
        }
        if (refresh) {
            return refreshPage(1, true).flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                    return observable;
                }
            });
        }
        return observable.doOnNext(new Action1<List<CloudAppItem>>() {
            @Override
            public void call(List<CloudAppItem> cloudAppItems) {
                cursor.setOffset(cursor.getOffset() + cloudAppItems.size());
            }
        });
    }

    public Observable<List<CloudAppItem>> getAllItems(final ItemType type, boolean refresh, final DataCursor cursor) {
        if (!userManager.isLoggedIn()) {
            return Observable.error(new LoginException());
        }
        final Observable<List<CloudAppItem>> observable = getAllItems(type, cursor);
        if (refresh) {
            return refreshPage(1, false).flatMap(new Func1<List<CloudAppItem>, Observable<List<CloudAppItem>>>() {
                @Override
                public Observable<List<CloudAppItem>> call(List<CloudAppItem> cloudAppItems) {
                    return observable;
                }
            });
        }
        return observable.doOnNext(new Action1<List<CloudAppItem>>() {
            @Override
            public void call(List<CloudAppItem> cloudAppItems) {
                cursor.setOffset(cursor.getOffset() + cloudAppItems.size());
            }
        });
    }

    @NonNull
    private Observable<List<CloudAppItem>> refreshPage(int page, boolean deleted) {
        return getListFromServer(makeListParams(page, ItemType.ALL, deleted, SERVER_ITEM_LIMIT))
                .flatMap(reduceList())
                .map(saveToDb())
                .toList();
    }

    private Observable<List<CloudAppItem>> getListFromServer(Map<String, String> options) {
        return cloudAppService.listItems(options)
                .retryWhen(new RxUtils.RetryWithDelay(2, 2000))
                .subscribeOn(Schedulers.io())
                .flatMap(new Func1<List<ItemModel>, Observable<List<CloudAppItem>>>() {
                    @Override
                    public Observable<List<CloudAppItem>> call(List<ItemModel> itemModels) {
                        return Observable.from(itemModels)
                                .map(convertItemModel()).toList();
                    }
                });
    }

    @NonNull
    private Func1<ItemModel, CloudAppItem> convertItemModel() {
        return new Func1<ItemModel, CloudAppItem>() {
            @Override
            public CloudAppItem call(ItemModel itemModel) {
                return new CloudAppItem(itemModel);
            }
        };
    }

    private Map<String, String> makeListParams(int page, ItemType type, boolean deleted, int pageSize) {
        Map<String, String> params = new HashMap<>();
        params.put("page", String.valueOf(page));
        params.put("per_page", String.valueOf(pageSize));
        if (type != ItemType.ALL) {
            params.put("type", type.toString().toLowerCase());
        }
        params.put("deleted", String.valueOf(deleted));

        return params;
    }

    private Map<String, RequestBody> makeMultipartParams(UploadModel uploadModel, CloudAppRequestBody body) {
        String filename = "file\"; filename=\"" + body.getFileName();
        Map<String, RequestBody> params = new LinkedHashMap<>();
        params.put("AWSAccessKeyId", createStringBody(uploadModel.getParams().getAWSAccessKeyId()));
        params.put("key", createStringBody(uploadModel.getParams().getKey()));
        params.put("acl", createStringBody(uploadModel.getParams().getAcl()));
        params.put("success_action_redirect", createStringBody(uploadModel.getParams().getSuccess_action_redirect()));
        params.put("signature", createStringBody(uploadModel.getParams().getSignature()));
        params.put("policy",createStringBody( uploadModel.getParams().getPolicy()));
        params.put(filename, body);
        return params;
    }

    private RequestBody createStringBody(String string) {
        return RequestBody.create(MediaType.parse("text/plain"), string);
    }

    @Subscribe
    public void onLogoutEvent(LogoutEvent event) {
        List<CloudAppItem> cloudAppItems = SugarRecord.listAll(CloudAppItem.class);
        SugarRecord.deleteInTx(cloudAppItems);
        Once.clearAll();
        CloudAppItem.executeQuery("DELETE FROM sqlite_sequence WHERE NAME = 'CLOUD_APP_ITEM'");
        CloudAppItem.executeQuery("VACUUM");
    }

    public boolean isSyncingAllItems() {
        return isSyncingAllItems;
    }

    public static class DataCursor implements Parcelable {
        public final int ITEM_LIST_LIMIT = 100;
        int offset = 0;
        String owner = "Dummy";

        public DataCursor(String owner) {
            this.owner = owner;
        }

        public int getLimit() {
            return ITEM_LIST_LIMIT;
        }

        public int getOffset() {
            return offset;
        }

        public void setOffset(int offset) {
            Timber.i("%s Cursor was %s now %s", owner,  this.offset, offset);
            this.offset = offset;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(this.offset);
        }

        public DataCursor() {
        }

        protected DataCursor(Parcel in) {
            this.offset = in.readInt();
        }

        public static final Creator<DataCursor> CREATOR = new Creator<DataCursor>() {
            public DataCursor createFromParcel(Parcel source) {
                return new DataCursor(source);
            }

            public DataCursor[] newArray(int size) {
                return new DataCursor[size];
            }
        };
    }
} public class UserManager {
    private static final String IS_LOGGED_IN = "IS_LOGGED_IN";
    private static final String EMAIL_ADDRESS = "EMAIL";
    private static final String PASSWORD = "PASS";

    private final DigestAuthenticator digestAuthenticator;
    private final CloudAppService cloudAppService;
    private final Bus bus;

    public UserManager(CloudAppService cloudAppService, DigestAuthenticator digestAuthenticator, Bus bus) {
        this.digestAuthenticator = digestAuthenticator;
        this.cloudAppService = cloudAppService;
        this.bus = bus;
        bus.register(this);
    }
    
    public boolean isLoggedIn() {
        return Hawk.get(IS_LOGGED_IN, false);
    }

    private void setLoggedIn(boolean bool) {
        Hawk.put(IS_LOGGED_IN, bool);
    }

    public Observable<Boolean> loginWith(final String userName, final String password) {
        digestAuthenticator.setCredentials(new UsernamePasswordCredentials(userName, password));
        
        return cloudAppService.getAccount()
                .onErrorResumeNext(new Func1<Throwable, Observable<? extends AccountModel>>() {
                    @Override
                    public Observable<? extends AccountModel> call(Throwable throwable) {
                        if (throwable instanceof HttpException) {
                            HttpException e = (HttpException) throwable;
                            LoginException exception = new LoginException();
                            if (e.code() > 500) {
                                exception = new LoginException("Service Unavailable – We’re temporarily offline for maintenance. Please try again later.", e);
                                exception.setCode(e.code());
                            } else if (e.code() > 400) {
                                exception = new LoginException("Digest authentication failed");
                                exception.setCode(e.code());
                            }
                            return Observable.error(exception);
                        } else {
                            return Observable.error(throwable);
                        }
                    }
                })
                .delaySubscription(1000, TimeUnit.MILLISECONDS)
                .map(new Func1<AccountModel, Boolean>() {
                    @Override
                    public Boolean call(AccountModel accountModel) {
                        if (accountModel != null && accountModel.getEmail().equals(userName)) {
                            putPassword(password);
                            putUserName(userName);
                            setLoggedIn(true);
                            return true;
                        }
                        setLoggedIn(false);
                        return false;
                    }
                })
                .subscribeOn(Schedulers.io());
                
    }
    
    private void putUserName(String userName) {
        Hawk.put(EMAIL_ADDRESS, userName);
    }
    
    private void putPassword(String password) {
        Hawk.put(PASSWORD, password);
    }

    public static String getUserName() {
        return Hawk.get(EMAIL_ADDRESS);
    }

    public static String getPassword() {
        return Hawk.get(PASSWORD);
    }

    public void logout() {
        Hawk.clear();
        bus.post(new LogoutEvent());
    }
} public class CloudAppItem extends SugarRecord implements Comparable<CloudAppItem>,Parcelable {

    @Unique
    private long itemId;
    private String href;
    private String name;
    private boolean isPrivate;
    private boolean isSubscribed;
    private String contentUrl;
    private String itemType;
    private long viewCounter;
    private String icon;
    private String url;
    private String remoteUrl;
    private String thumbnailUrl;
    private String downloadUrl;
    private String source;
    boolean favorite;
    String ownerId;
    long contentLength;
    private String createdAt;
    private String updatedAt;
    private String deletedAt;
    private String lastViewedAt;

    public CloudAppItem(ItemModel itemModel) {
        this.itemId = itemModel.id;
        this.href = itemModel.href;
        this.name = itemModel.name;
        this.isPrivate = itemModel.isPrivate;
        this.isSubscribed = itemModel.isSubscribed;
        this.contentUrl = itemModel.contentUrl;
        this.itemType = itemModel.itemType;
        this.viewCounter = itemModel.viewCounter;
        this.icon = itemModel.icon;
        this.url = itemModel.url;
        this.remoteUrl = itemModel.remoteUrl;
        this.thumbnailUrl = itemModel.thumbnailUrl;
        this.downloadUrl = itemModel.downloadUrl;
        this.source = itemModel.source;
        this.favorite = itemModel.favorite;
        this.ownerId = itemModel.ownerId;
        this.contentLength = itemModel.contentLength;
        this.createdAt = String.valueOf(formatDate(itemModel.createdAt));
        this.updatedAt = String.valueOf(formatDate(itemModel.updatedAt));
        this.deletedAt = String.valueOf(formatDate(itemModel.deletedAt));
        this.lastViewedAt = String.valueOf(formatDate(itemModel.lastViewedAt));
    }

    public CloudAppItem() {
    }

    public long getItemId() {
        return itemId;
    }

    public String getDownloadUrl() {
        return downloadUrl;
    }

    public String getHref() {
        return href;
    }

    
    public String getName() {
        return name;
    }

    
    public boolean isPrivate() {
        return isPrivate;
    }

    
    public boolean isSubscribed() {
        return isSubscribed;
    }

    public boolean isTrashed() {
        return !deletedAt.equals(String.valueOf(-1));
    }

    public String getUrl() {
        return url == null? remoteUrl : url;
    }

    
    public String getContentUrl() {
        return contentUrl;
    }

    
    public ItemType getItemType() {
        try {
            return ItemType.valueOf(itemType.toUpperCase());
        } catch (IllegalArgumentException e) {
            return ItemType.UNKNOWN;
        }
    }

    
    public long getViewCounter() {
        return viewCounter;
    }

    
    public String getIcon() {
        return icon;
    }

    
    public String getRemoteUrl() {
        return remoteUrl;
    }

    
    public String getSource() {
        return source;
    }

    
    public String getCreatedAt() {
        return createdAt;
    }

    
    public String getUpdatedAt() {
        return updatedAt;
    }

    
    public String getDeletedAt() {
        return deletedAt;
    }

    
    public String getLastViewedAt() {
        return lastViewedAt;
    }

    
    public String getThumbnailUrl() {
        return thumbnailUrl;
    }

    public long getContentLength() {
        return contentLength;
    }

    public String getFormattedCreatedAt(Context context)  {

        return formatDateTime(context, Long.parseLong(createdAt), FORMAT_SHOW_TIME|FORMAT_SHOW_YEAR|FORMAT_SHOW_DATE);
    }

    
    public String getFormattedUpdatedAt()  {
        try {
            return updatedAt ==null?null:DateFormat.getDateInstance()
                    .format(CloudAppUtils.format.parse(updatedAt));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    
    public String getFormattedDeletedAt() {
        try {
            return deletedAt ==null?null:DateFormat.getDateInstance()
                    .format(CloudAppUtils.format.parse(deletedAt));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    public boolean isFavorite() {
        return favorite;
    }

    public String getOwnerId() {
        return ownerId;
    }

    @Override
    public int compareTo(CloudAppItem another) {
        CloudAppItem lhs = this;
        if (another != null) {
            if (lhs.equals(another)) return 0;
            if (lhs.getItemId() < another.getItemId()) return 1;
            if (lhs.getItemId() < another.getItemId()) return -1;
        }
        return -1;
    }

    public enum ItemType {
        ALL, DELETED, AUDIO, BOOKMARK, IMAGE, UNKNOWN, VIDEO, ARCHIVE, TEXT
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setDeletedAt(String deletedAt) {
        this.deletedAt = deletedAt;
    }

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

        CloudAppItem item = (CloudAppItem) o;

        return itemId == item.itemId;

    }

    public int hash() {
        int result = href.hashCode();
        result = 31 * result + createdAt.hashCode();
        result = 31 * result + updatedAt.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "CloudAppItem{" +
                "name='" + name + '\'' +
                ", href='" + href + '\'' +
                ", itemId=" + itemId +
                '}';
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(this.itemId);
        dest.writeString(this.href);
        dest.writeString(this.name);
        dest.writeByte(isPrivate ? (byte) 1 : (byte) 0);
        dest.writeByte(isSubscribed ? (byte) 1 : (byte) 0);
        dest.writeString(this.contentUrl);
        dest.writeString(this.itemType);
        dest.writeLong(this.viewCounter);
        dest.writeString(this.icon);
        dest.writeString(this.url);
        dest.writeString(this.remoteUrl);
        dest.writeString(this.thumbnailUrl);
        dest.writeString(this.downloadUrl);
        dest.writeString(this.source);
        dest.writeByte(favorite ? (byte) 1 : (byte) 0);
        dest.writeString(this.ownerId);
        dest.writeLong(this.contentLength);
        dest.writeString(this.createdAt);
        dest.writeString(this.updatedAt);
        dest.writeString(this.deletedAt);
        dest.writeString(this.lastViewedAt);
    }

    protected CloudAppItem(Parcel in) {
        this.itemId = in.readLong();
        this.href = in.readString();
        this.name = in.readString();
        this.isPrivate = in.readByte() != 0;
        this.isSubscribed = in.readByte() != 0;
        this.contentUrl = in.readString();
        this.itemType = in.readString();
        this.viewCounter = in.readLong();
        this.icon = in.readString();
        this.url = in.readString();
        this.remoteUrl = in.readString();
        this.thumbnailUrl = in.readString();
        this.downloadUrl = in.readString();
        this.source = in.readString();
        this.favorite = in.readByte() != 0;
        this.ownerId = in.readString();
        this.contentLength = in.readLong();
        this.createdAt = in.readString();
        this.updatedAt = in.readString();
        this.deletedAt = in.readString();
        this.lastViewedAt = in.readString();
    }

    public static final Parcelable.Creator<CloudAppItem> CREATOR = new Parcelable.Creator<CloudAppItem>() {
        public CloudAppItem createFromParcel(Parcel source) {
            return new CloudAppItem(source);
        }

        public CloudAppItem[] newArray(int size) {
            return new CloudAppItem[size];
        }
    };
} public class IntentBridge extends Activity {
    public static final String AWAITING_UPLOAD = "com.tevinjeffrey.vapor.services.ACTION_TYPE";
    public final static String FILE_SELECT = "com.tevinjeffrey.vapor.services.FileSelect";
    public final static String FILE_TYPE = "com.tevinjeffrey.vapor.services.FILE_TYPE";

    public final static String FILE_BOOKMARK = "com.tevinjeffrey.vapor.services.FILE_BOOKMARK";
    public final static String FILE_TEXT = "com.tevinjeffrey.vapor.services.FILE_TEXT";
    public final static String FILE_OTHER = "com.tevinjeffrey.vapor.services.FILE_OTHER";

    private static final int FILE_SELECT_CODE = 42;

    @Inject
    RefCountManager refCountManager;
    @Inject
    UserManager userManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        VaporApp.uiComponent(getApplicationContext()).inject(this);

        // If the user is not logged in, but tries to upload a file anyway. This should catch that
        // intent and start the login process instead.
        if (!userManager.isLoggedIn()) {
            Intent intent = new Intent(this, FilesActivity.class);
            startActivity(intent);
            Toast.makeText(this, "Please log in first!", Toast.LENGTH_LONG).show();
            finish();
            return;
        }

        //Check if the required permissions are granted.
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
                || ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            Intent intent = new Intent(this, FilesActivity.class);
            startActivity(intent);
            Toast.makeText(this, "Please enable the required permissions!", Toast.LENGTH_LONG).show();
            finish();
            return;
        }

        Intent incomingIntent = getIntent();
        // True when the user attempts to select a file from withing the app.
        if (incomingIntent.getAction().equals(FILE_SELECT)) {
            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("*/*");
            startActivityForResult(intent, FILE_SELECT_CODE);
        // True when an external app attempts to SEND data to the app
        } else if(incomingIntent.getAction().equals(Intent.ACTION_SEND)) {
            handleIntent(incomingIntent);
        }

    }

    private void handleIntent(Intent incomingIntent) {
        final Intent outgoingIntent = new Intent(this, UploadService.class);
        // Prepare the Intent to be sent to the UploadService. ACTION_UPLOAD to tell the service we
        // attempting to upload a file.
        outgoingIntent.putExtra(UploadService.ACTION_TYPE, UploadService.ACTION_UPLOAD);

        // Get the type of the file to determine what to do. Text files are treated differently from
        // regular audio, video and other binaries. Text files could be a URL or a UTF-8 string.
        String type = incomingIntent.getType();
        if (type == null) {
            type = FileUtils.getMimeType(this, incomingIntent.getData());
        }
        if (type.equals("text/plain")) {
            outgoingIntent.putExtra(FILE_TYPE, FILE_TEXT);
            CharSequence text = incomingIntent.getStringExtra(Intent.EXTRA_TEXT);
            if (text == null) {
                Timber.e("Error text_extra was null");
                return;
            }
            if (Patterns.WEB_URL.matcher(text).matches()) {
                outgoingIntent.putExtra(FILE_TYPE, FILE_BOOKMARK);
            }
            outgoingIntent.putExtra(Intent.EXTRA_TEXT, text);
            startService(outgoingIntent);
            finish();
        } else {

            // Get the file from either data or EXTRA_STREAM. Depends on entirely on who's sending.
            final Uri fileUri = incomingIntent.getData() == null ? (Uri) incomingIntent.getParcelableExtra(Intent.EXTRA_STREAM) :
                    incomingIntent.getData();
            outgoingIntent.putExtra(FILE_TYPE, FILE_OTHER);

            // Google photos is serious about privacy and does not allow URI's to be passed around
            // component within the app. So we must first write the file publicly to the disk. Then
            // retrieve the URI for that location. We pass the uri to the UploadService, which will
            // alert use when the file a this URI has been successfully uploaded. After the upload
            // complete's we delete the file.
            if (FileUtils.isGooglePhotosUri(fileUri)) {
                Observable.defer(new Func0<Observable<Uri>>() {
                    @Override
                    public Observable<Uri> call() {
                        return Observable.just(getImageUrlWithAuthority(fileUri));
                    }
                })
                        .subscribeOn(Schedulers.io())
                        .subscribe(new Action1<Uri>() {
                            @Override
                            public void call(Uri uri) {
                                refCountManager.addUri(uri);
                                outgoingIntent.setData(uri)
                                    .putExtra(Intent.EXTRA_STREAM, uri);
                                startService(outgoingIntent);
                                finish();

                            }
                        });
            // Check to see if the is local to the device as some apps may attempt to serve a file
            // that's on a remote server.
            } else if (FileUtils.isLocal(fileUri.toString())) {
                outgoingIntent.setData(fileUri)
                    .putExtra(Intent.EXTRA_STREAM, fileUri);
                startService(outgoingIntent);
                finish();
            } else {
                Toast.makeText(this, "Vapor cannot upload that file", Toast.LENGTH_SHORT).show();
                finish();
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == FILE_SELECT_CODE) {
            // User selects a file to upload from the document viewer.
            if (resultCode == RESULT_OK) {
                handleIntent(data);
            } else if (resultCode == RESULT_CANCELED) {
                finish();
            }
        }
    }

    /* http://stackoverflow.com/questions/30527045/choosing-photo-using-new-google-photos-app-is-broken */
    public Uri getImageUrlWithAuthority(Uri uri) {
        InputStream is = null;
        if (uri.getAuthority() != null) {
            try {
                is = this.getContentResolver().openInputStream(uri);
                Bitmap bmp = BitmapFactory.decodeStream(is);
                return writeToTempImageAndGetPathUri(bmp);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }finally {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public Uri writeToTempImageAndGetPathUri(Bitmap inImage) {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes);
        String path = MediaStore.Images.Media.insertImage(this.getContentResolver(), inImage, "Title", null);
        return Uri.parse(path);
    }
} public class ImageActivity extends Activity {


    @Bind(R.id.imageView)
    ImageView imageView;
    PhotoViewAttacher mAttacher;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        getWindow().addFlags(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_image);
        ButterKnife.bind(this);

        final MaterialDialog progress = new MaterialDialog.Builder(this).content("Please wait...").progress(true, 0).show();

        Glide.with(getApplicationContext()).load(getIntent().getData())
                .listener(new RequestListener<Uri, GlideDrawable>() {
                    @Override
                    public boolean onException(Exception e, Uri model, Target<GlideDrawable> target, boolean isFirstResource) {
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(GlideDrawable resource, Uri model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
                        progress.dismiss();
                        imageView.setImageDrawable(resource);
                        mAttacher = new PhotoViewAttacher(imageView);
                        return false;
                    }
                })
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .into(imageView);

    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        ButterKnife.unbind(this);
    }
} public class SettingsActivity extends AppCompatPreferenceActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.base_preference);
        setToolbar();
        getFragmentManager().beginTransaction().replace(R.id.content_frame, new SettingsFragment()).commit();
    }

    private void setToolbar() {
        Toolbar toolbar = ButterKnife.findById(this, R.id.toolbar);
        setSupportActionBar(toolbar);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onBackPressed();
            }
        });

        ActionBar actionBar = getSupportActionBar();

        if (actionBar != null) {
            actionBar.setTitle(getString(R.string.settings));
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setDisplayShowHomeEnabled(true);
        }
        toolbar.setTitleTextAppearance(this, R.style.ToolbarTitleStyle);
        toolbar.setSubtitleTextAppearance(this, R.style.ToolbarSubtitleStyle_TextApperance);
    }


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    public static class SettingsFragment extends PreferenceFragment {

        private SettingsActivity getParentActivity() {
            return (SettingsActivity) getActivity();
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
//            getObjectGraph().inject(this);

            addPreferencesFromResource(R.xml.settings);
        }



        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            ListView list = (ListView) getView().findViewById(android.R.id.list);
            list.setDivider(new ColorDrawable(Color.parseColor("#e6e6e6")));
            list.setDividerHeight((int) getParentActivity().getResources().getDisplayMetrics().density);

            super.onActivityCreated(savedInstanceState);
        }
    }
} public class FilesFragment extends MVPFragment implements FilesFragmentView, SwipeRefreshLayout.OnRefreshListener {
    private static final String ITEM_TYPE = "ITEM_TYPE";
    @Bind(R.id.files_recyclerview) RecyclerView mRecyclerView;
    @Bind(R.id.empty_view) LinearLayout mEmptyView;
    @Bind(R.id.progressBar) ProgressBar mProgressBar;
    @Bind(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;

    @Inject Bus bus;

    @State
    FilesFragmentViewState mViewState = new FilesFragmentViewState();

    @State
    DataManager.DataCursor cursor;

    private ItemType mItemType;

    private List<CloudAppItem> mListDataSet;
    private boolean isViewShown;

    public static FilesFragment newInstance(ItemType itemType) {
        FilesFragment fragment = new FilesFragment();
        Bundle bundle = new Bundle();
        bundle.putSerializable(ITEM_TYPE, itemType);
        fragment.setArguments(bundle);
        return fragment;
    }

    public FilesFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        setRetainInstance(true);
        super.onCreate(savedInstanceState);
        VaporApp.uiComponent(getParentActivity()).inject(this);
        if (getArguments() != null) {
            mItemType = (ItemType) getArguments().getSerializable(ITEM_TYPE);
        }
    }

    @Override
    public android.view.View onCreateView(LayoutInflater inflater, ViewGroup container,
                                          Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        android.view.View view = inflater.inflate(R.layout.fragment_files_list, container, false);
        ButterKnife.bind(this, view);
        return view;
    }

    @Override
    public void onViewCreated(android.view.View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (mPresenter == null) {
            setPresenter(getTypedPresenter());
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //Attach view to presenter
        mViewState.apply(this, savedInstanceState != null);

        if (getCursor() == null) {
            cursor = new DataManager.DataCursor(getPresenter().getClass().getSimpleName());
        }
        mPresenter.attachView(this);
        if (getPresenter().isNotLoading()) {
            getPresenter().loadData(true, false, true);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        bus.register(this);

    }

    @Override
    public void onPause() {
        super.onPause();
        bus.unregister(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mPresenter.detachView();
    }

    private FilesPresenter getPresenter() {
        return (FilesPresenter) mPresenter;
    }

    public void setPresenter(Presenter filesPresenter) {
        mPresenter = filesPresenter;
    }

    @Override
    public void showLoading(final boolean pullToRefresh) {
        mViewState.isRefreshing = pullToRefresh;
        mSwipeRefreshLayout.post(new Runnable() {
            @Override
            public void run() {
                if (mSwipeRefreshLayout != null) {
                    mSwipeRefreshLayout.setRefreshing(pullToRefresh);
                }
            }
        });
    }

    @Override
    public void setData(List<CloudAppItem> data) {
        if(mListDataSet.equals(data) && mListDataSet.size() != 0){
           return;
        }
        Iterator<CloudAppItem> itemIterator = data.iterator();
        while (itemIterator.hasNext()) {
            CloudAppItem currentItem = itemIterator.next();
            for (int i = 0; i < mListDataSet.size(); i++) {
                if (mListDataSet.get(i).equals(currentItem)) {
                    // Only updated items that have changed
                    if (mListDataSet.get(i).hash() != currentItem.hash()) {
                        mListDataSet.set(i, currentItem);
                        mRecyclerView.getAdapter().notifyItemChanged(i);
                    }
                    itemIterator.remove();
                    break;
                }
            }
        }
        appendData(data);
    }

    public void appendData(List<CloudAppItem> data) {
        mListDataSet.addAll(data);
        if (mListDataSet.size() == 0 && getPresenter().shouldShowEmpty()) {
            showLayout(View.LayoutType.EMPTY);
        }

        if (mListDataSet.size() > 0) {
            showLayout(View.LayoutType.LIST);
        }

        mViewState.data = mListDataSet;
        if (getParentActivity().getPresenter().getNavContext() != FilesActivityPresenter.NavContext.POPULAR &&
                getParentActivity().getPresenter().getNavContext() != FilesActivityPresenter.NavContext.TRASH) {
            if (!isSorted(mListDataSet, false)) {
                Collections.sort(mListDataSet);
                mRecyclerView.getAdapter().notifyDataSetChanged();
            }
        }
        if (data.size() != 0) {
            mRecyclerView.getAdapter().notifyItemRangeInserted(mListDataSet.size(), data.size());
        }
    }

    @Override
    public boolean isVisibleInPager() {
        return isViewShown;
    }

    public static <E extends Comparable<? super E>> boolean isSorted(List<E> list, boolean asc) {
        if (list.size() == 0) return true;
        for (int i = 0; i < list.size(); i++) {
            if (i != list.size() -1) {
                int comp = list.get(i).compareTo(list.get(i + 1));
                int to = asc ? -1 : 1;
                if (comp != to && comp != 0) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public void showError(Throwable t) {
        String message;
        Resources resources = getResources();

        if (t instanceof UnknownHostException) {
            message = resources.getString(R.string.no_internet);
        } else if (t instanceof SocketTimeoutException) {
            message = resources.getString(R.string.timed_out);
        } else if (t instanceof LoginException) {
            message = resources.getString(R.string.user_not_logged_in);
            showLayout(LayoutType.LOADING);
        } else {
            Timber.e(t, "show error");
            message = t.getMessage();
        }
        mViewState.errorMessage = message;
        Snackbar.make(getView(), message, Snackbar.LENGTH_INDEFINITE)
                .setAction("Retry", new android.view.View.OnClickListener() {
            @Override
            public void onClick(android.view.View v) {
                showLoading(true);
            }
        }).show();
    }

    public void initRecyclerView() {
        GridLayoutManager layoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        layoutManager.setSmoothScrollbarEnabled(true);
        mRecyclerView.setLayoutManager(layoutManager);
        mRecyclerView.setHasFixedSize(true);
        mRecyclerView.addItemDecoration(new MarginDecoration(getParentActivity()));
        if (mListDataSet == null) {
            mListDataSet = new ArrayList<>(100);
        }

        if (mRecyclerView.getAdapter() == null) {
         /*   AlphaInAnimationAdapter animationAdapter = new AlphaInAnimationAdapter();
            animationAdapter.setFirstOnly(false);
            animationAdapter.setDuration(50);*/
            mRecyclerView.setAdapter(new FilesFragmentAdapter(mListDataSet, getParentActivity()));
        }

        mRecyclerView.addOnScrollListener(new EndlessRecyclerOnScrollListener(layoutManager) {
            @Override
            public void onLoadMore(int currentPage) {
                if(getPresenter().isNotLoading()) {
                    getPresenter().loadData(false, false, true);
                }
            }
        });
    }

    public void initSwipeLayout() {
        mSwipeRefreshLayout.setOnRefreshListener(this);
        mSwipeRefreshLayout.setSize(SwipeRefreshLayout.DEFAULT);
        mSwipeRefreshLayout.setColorSchemeResources(R.color.primary, R.color.accent);
    }

    @Override
    public void showLayout(LayoutType type) {
        mViewState.layoutType = type;
        switch (type) {
            case EMPTY:
                showRecyclerView(GONE);
                showLoadingView(GONE);
                showEmptyLayout(VISIBLE);
                break;
            case LIST:
                showEmptyLayout(GONE);
                showLoadingView(GONE);
                showRecyclerView(VISIBLE);
                break;
            case LOADING:
                showEmptyLayout(GONE);
                showRecyclerView(GONE);
                showLoadingView(VISIBLE);
                break;
            default:
                throw new RuntimeException("Unknown type: " + type);
        }
    }

    @Override
    public DataManager.DataCursor getCursor() {
        return cursor;
    }

    private void showEmptyLayout(int visibility) {
        if (mEmptyView.getVisibility() != visibility)
            mEmptyView.setVisibility(visibility);
    }

    private void showRecyclerView(int visibility) {
        if (mRecyclerView.getVisibility() != visibility)
            mRecyclerView.setVisibility(visibility);
    }

    private void showLoadingView(int visibility) {
        if (mProgressBar.getVisibility() != visibility)
            mProgressBar.setVisibility(visibility);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        ButterKnife.unbind(this);
    }

    private Presenter getTypedPresenter() {
        Presenter presenter;
        switch (mItemType) {
            case ALL:
                RecentPresenter recentPresenter = VaporApp.uiComponent(getParentActivity()).getRecentPresenter();
                VaporApp.uiComponent(getParentActivity()).inject(recentPresenter);
                presenter = recentPresenter;
                break;
            case IMAGE:
                ImagePresenter imagePresenter = VaporApp.uiComponent(getParentActivity()).getImagePresenter();
                VaporApp.uiComponent(getParentActivity()).inject(imagePresenter);
                presenter = imagePresenter;
                break;
            case VIDEO:
                VideoPresenter videoPresenter = VaporApp.uiComponent(getParentActivity()).getVideoPresenter();
                VaporApp.uiComponent(getParentActivity()).inject(videoPresenter);
                presenter = videoPresenter;
                break;
            case ARCHIVE:
                ArchivePresenter archivePresenter = VaporApp.uiComponent(getParentActivity()).getArchivePresenter();
                VaporApp.uiComponent(getParentActivity()).inject(archivePresenter);
                presenter = archivePresenter;                
                break;
            case TEXT:
                TextPresenter textPresenter = VaporApp.uiComponent(getParentActivity()).getTextPresenter();
                VaporApp.uiComponent(getParentActivity()).inject(textPresenter);
                presenter = textPresenter;
                break;
            case AUDIO:
                AudioPresenter audioPresenter = VaporApp.uiComponent(getParentActivity()).getAudioPresenter();
                VaporApp.uiComponent(getParentActivity()).inject(audioPresenter);
                presenter = audioPresenter;
                break;
            case BOOKMARK:
                BookmarkPresenter bookmarkPresenter = VaporApp.uiComponent(getParentActivity()).getBookmarkPresenter();
                VaporApp.uiComponent(getParentActivity()).inject(bookmarkPresenter);
                presenter = bookmarkPresenter;
                break;
            case UNKNOWN:
                UnknownPresenter unknownPresenter = VaporApp.uiComponent(getParentActivity()).getUnknownPresenter();
                VaporApp.uiComponent(getParentActivity()).inject(unknownPresenter);
                presenter = unknownPresenter;
                break;
            default:
                recentPresenter = VaporApp.uiComponent(getParentActivity()).getRecentPresenter();
                VaporApp.uiComponent(getParentActivity()).inject(recentPresenter);
                presenter = recentPresenter;
        }
        return presenter;
    }

    @Override
    public String toString() {
        return "FilesFragment{" +
                "presenter=" + getPresenter().getClass().getSimpleName() +
                '}';
    }

    @Override
    public void onRefresh() {
        if (getPresenter().isNotLoading()) {
            getPresenter().loadData(true, true, false);
        }
    }

    @Subscribe
    public void onRenameEvent(RenameEvent event){
        if (mListDataSet != null) {
            for (int i = 0 ;i < mListDataSet.size(); i++) {
                if (mListDataSet.get(i).getItemId() == event.getItem().getItemId()) {
                    mListDataSet.set(i, event.getItem());
                    mRecyclerView.getAdapter().notifyItemChanged(i);
                }
            }
        }
    }

    @Subscribe
    public void onDeleteEvent(DeleteEvent event){
        if (mListDataSet != null) {
            for (int i = 0 ;i < mListDataSet.size(); i++) {
                if (mListDataSet.get(i).getItemId() == event.getItem().getItemId() && event.getItem().getDeletedAt() != null) {
                    mListDataSet.remove(i);
                    mRecyclerView.getAdapter().notifyItemRemoved(i);
                }
            }
        }
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        isViewShown = getView() != null && isVisibleToUser;
    }

    @Subscribe
    public void onUploadEvent(UploadEvent event){
        if (mListDataSet != null) {
            mListDataSet.add(0, event.getItem());
            mRecyclerView.getAdapter().notifyItemInserted(0);
        }
    }
} public class LoginActivity extends AppCompatActivity implements LoginView {

    @Bind(R.id.login_field_email)
    EditText loginEmail;
    @Bind(R.id.login_wrapper_email)
    TextInputLayout loginWrapperEmail;
    @Bind(R.id.login_field_password)
    EditText loginPassword;
    @Bind(R.id.login_wrapper_password)
    TextInputLayout loginWrapperPassword;
    @Bind(R.id.login_button)
    Button loginButton;
    @Bind(R.id.login_field_container)
    RelativeLayout loginFieldContainer;
    @Bind(R.id.progressBar)
    ProgressBar loginProgressBar;

    @Inject
    LoginPresenter loginPresenter;

    @Inject
    Bus bus;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        VaporApp.uiComponent(this).inject(this);
        setContentView(R.layout.activity_login);
        loginPresenter.attachView(this);
        ButterKnife.bind(this);
        bus.register(this);

        if (BuildConfig.DEBUG) {
            loginPassword.setText(BuildConfig.PASS);
            loginEmail.setText(BuildConfig.EMAIL);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        bus.unregister(this);
        loginPresenter.detachView();
        ButterKnife.unbind(this);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_login, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @OnClick(R.id.login_button)
    public void onLoginClick(View view) {
        String userEmail = loginEmail.getText().toString();
        String userPass = loginPassword.getText().toString();
        loginWrapperEmail.setErrorEnabled(false);
        loginWrapperPassword.setErrorEnabled(false);

        if (isSignInFieldValid()) {
            showLoading(true);
            loginPresenter.tryLogin(userEmail, userPass);
        }
    }

    @Override
    public void showLoading(boolean loading) {
        loginProgressBar.setIndeterminate(loading);
        loginProgressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
        if (loading) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                // previously visible view
                final View myView = loginFieldContainer;

                // get the center for the clipping circle
                int cx = (myView.getLeft() + myView.getRight()) / 2;
                int cy = (myView.getTop() + myView.getBottom()) / 2;

                // get the initial radius for the clipping circle
                int initialRadius = myView.getWidth();

                // create the animation (the final radius is zero)
                Animator anim =
                        ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0);

                // make the view invisible when the animation is done
                anim.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        myView.setVisibility(View.INVISIBLE);
                    }
                });

                // start the animation
                anim.start();
            } else {
                ViewCompat.animate(loginFieldContainer).alpha(0).start();
            }
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

                // previously invisible view
                View myView = loginFieldContainer;

                // get the center for the clipping circle
                int cx = (myView.getLeft() + myView.getRight()) / 2;
                int cy = (myView.getTop() + myView.getBottom()) / 2;

                // get the final radius for the clipping circle
                int finalRadius = Math.max(myView.getWidth(), myView.getHeight());

                // create the animator for this view (the start radius is zero)
                Animator anim =
                        ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0, finalRadius);

                // make the view visible and start the animation
                myView.setVisibility(View.VISIBLE);
                anim.start();
            } else {
                ViewCompat.animate(loginFieldContainer).alpha(1).start();
/*
                loginFieldContainer.setVisibility(View.VISIBLE);
*/
            }
        }
    }

    @Override
    public void loginSuccessful() {
        Intent intent = new Intent(this, IntroActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        startActivity(intent);
        finish();
    }

    @Override
    public void showError(Throwable t) {
        String message;
        Resources resources = getResources();

        if (t instanceof UnknownHostException) {
            message = resources.getString(R.string.no_internet);
        } else if (t instanceof SocketTimeoutException) {
            message = resources.getString(R.string.timed_out);
        } else if (t instanceof LoginException && ((LoginException) t).getCode() < 500) {
            loginWrapperEmail.setErrorEnabled(true);
            loginWrapperEmail.setError(" ");
            loginWrapperPassword.setErrorEnabled(true);
            loginWrapperPassword.setError(getString(R.string.email_password_incorrect));
            Timber.e(t, "Error while logging in.");
            return;
        } else {
            message = t.getMessage();
        }
        Snackbar.make(this.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show();
    }

    private boolean isSignInFieldValid() {

        String userEmail = loginEmail.getText().toString();
        String userPass = loginPassword.getText().toString();
        
        if (TextUtils.isEmpty(userEmail)) {
            loginWrapperEmail.setErrorEnabled(true);
            loginWrapperEmail.setError(getString(R.string.empty_email));
            return false;
        } else if (!VaporUtils.isValidEmail(userEmail)) {
            loginWrapperEmail.setErrorEnabled(true);
            loginWrapperEmail.setError(getString(R.string.invalid_email));
            return false;
        } else {
            loginWrapperEmail.setErrorEnabled(false);

        }
        if (TextUtils.isEmpty(userPass)) {
            loginWrapperPassword.setErrorEnabled(true);
            loginWrapperPassword.setError(getString(R.string.empty_password));
            return false;
        } else {
            loginWrapperPassword.setErrorEnabled(false);
        }

        return true;
    }

    @Override
    public void onBackPressed() {
        moveTaskToBack(true);
    }

    @OnClick(R.id.tos)
    public void tosDialog(View v) {
        String url = "https://github.com/cloudapp/policy/blob/master/terms-of-service.md";
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setData(Uri.parse(url));
        startActivity(i);
    }
} public interface ItemClickListener<D, V> {
    void onItemClicked(D data, V view);
} public class ScrimUtil {

    private ScrimUtil() { }

    /**
     * Creates an approximated cubic gradient using a multi-stop linear gradient. See
     * <a href="https://plus.google.com/+RomanNurik/posts/2QvHVFWrHZf">this post</a> for more
     * details.
     */
    public static Drawable makeCubicGradientScrimDrawable(@ColorInt int baseColor,
                                                          int numStops,
                                                          int gravity) {
        numStops = Math.max(numStops, 2);

        PaintDrawable paintDrawable = new PaintDrawable();
        paintDrawable.setShape(new RectShape());

        final int[] stopColors = new int[numStops];

        int alpha = Color.alpha(baseColor);

        for (int i = 0; i < numStops; i++) {
            float x = i * 1f / (numStops - 1);
            float opacity = MathUtils.constrain(0, 1, (float) Math.pow(x, 3));
            stopColors[i] = ColorUtils.modifyAlpha(baseColor, (int) (alpha * opacity));
        }

        final float x0, x1, y0, y1;
        switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.LEFT:
                x0 = 1;
                x1 = 0;
                break;
            case Gravity.RIGHT:
                x0 = 0;
                x1 = 1;
                break;
            default:
                x0 = 0;
                x1 = 0;
                break;
        }
        switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.TOP:
                y0 = 1;
                y1 = 0;
                break;
            case Gravity.BOTTOM:
                y0 = 0;
                y1 = 1;
                break;
            default:
                y0 = 0;
                y1 = 0;
                break;
        }

        paintDrawable.setShaderFactory(new ShapeDrawable.ShaderFactory() {
            @Override
            public Shader resize(int width, int height) {
                LinearGradient linearGradient = new LinearGradient(
                        width * x0,
                        height * y0,
                        width * x1,
                        height * y1,
                        stopColors, null,
                        Shader.TileMode.CLAMP);
                return linearGradient;
            }
        });

        return paintDrawable;
    }
} public class VaporUtils {
    public static boolean isValidWebAddress(CharSequence target) {
        return !TextUtils.isEmpty(target) && Patterns.DOMAIN_NAME.matcher(target).matches();
    }

    public static boolean isValidEmail(CharSequence target) {
        return !TextUtils.isEmpty(target) && android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches();
    }

    public static String humanReadableByteCount(long bytes, boolean si) {
        int unit = si ? 1000 : 1024;
        if (bytes < unit) return bytes + " B";
        int exp = (int) (Math.log(bytes) / Math.log(unit));
        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
    }

    public static void openLink(Context context, String url) {
        Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        i.setData(Uri.parse(url));
        context.startActivity(i);
    }

    public static Drawable getTypedDrawable(Context context, CloudAppItem cloudAppItem) {
        CloudAppItem.ItemType type = cloudAppItem.getItemType();
        Drawable drawable = null;
        if (type == IMAGE) {
            drawable = ContextCompat.getDrawable(context, R.drawable.ic_photo);
        } else if (type == VIDEO) {
            drawable = ContextCompat.getDrawable(context, R.drawable.ic_file_video);
        } else if (type == ARCHIVE) {
            drawable = ContextCompat.getDrawable(context, R.drawable.ic_zip_box);
        } else if (type == BOOKMARK) {
            drawable = ContextCompat.getDrawable(context, R.drawable.ic_bookmark);
        } else if (type == AUDIO) {
            drawable = ContextCompat.getDrawable(context, R.drawable.ic_headset);
        } else if (type == TEXT) {
            drawable = ContextCompat.getDrawable(context, R.drawable.ic_file_document);
        } else if (type == UNKNOWN) {
            drawable = ContextCompat.getDrawable(context, R.drawable.ic_file_cloud);
        }
        return drawable;
    }

    public static void setTypedImageView(CloudAppItem cloudAppItem, final ImageView imageView, Drawable placeholder, boolean thumbnail) {
        if (cloudAppItem.getItemType() == IMAGE) {
            Glide.with(imageView.getContext().getApplicationContext())
                    .load(thumbnail?cloudAppItem.getThumbnailUrl():cloudAppItem.getRemoteUrl())
                    .crossFade()
                    .centerCrop()
                    .diskCacheStrategy(DiskCacheStrategy.ALL)
                    .priority(Priority.IMMEDIATE)
                    .placeholder(placeholder)
                    .listener(new RequestListener<String, GlideDrawable>() {
                        @Override
                        public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
                            if (e != null) {
                                Timber.e(e, "Failed to load image");
                            }
                            return false;
                        }

                        @Override
                        public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
                            imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);

                            return false;
                        }
                    })
                    .into(imageView);
            imageView.setScaleType(ImageView.ScaleType.CENTER);
        } else {
            Drawable drawable = getTypedDrawable(imageView.getContext(), cloudAppItem);
            RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
            layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
            imageView.setLayoutParams(layoutParams);
            imageView.setImageDrawable(drawable);
        }
    }
}
import android.content.ClipboardManager;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.NavigationView;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.InputType;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import com.flipboard.bottomsheet.BottomSheetLayout;
import com.flipboard.bottomsheet.commons.IntentPickerSheetView;
import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import com.tevinjeffrey.vapor.R;
import com.tevinjeffrey.vapor.VaporApp;
import com.tevinjeffrey.vapor.customviews.TouchImageView;
import com.tevinjeffrey.vapor.events.DeleteEvent;
import com.tevinjeffrey.vapor.events.RenameEvent;
import com.tevinjeffrey.vapor.okcloudapp.DataManager;
import com.tevinjeffrey.vapor.okcloudapp.UserManager;
import com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem;
import com.tevinjeffrey.vapor.services.IntentBridge;
import com.tevinjeffrey.vapor.ui.ImageActivity;
import com.tevinjeffrey.vapor.ui.SettingsActivity;
import com.tevinjeffrey.vapor.ui.files.fragments.FilesFragment;
import com.tevinjeffrey.vapor.ui.login.LoginActivity;
import com.tevinjeffrey.vapor.ui.utils.ItemClickListener;
import com.tevinjeffrey.vapor.utils.ScrimUtil;
import com.tevinjeffrey.vapor.utils.VaporUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import butterknife.Bind;
import butterknife.ButterKnife;
import icepick.Icepick;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import timber.log.Timber;
import static android.support.design.widget.TabLayout.MODE_SCROLLABLE;
import static com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem.ItemType.ALL;
import static com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem.ItemType.ARCHIVE;
import static com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem.ItemType.AUDIO;
import static com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem.ItemType.BOOKMARK;
import static com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem.ItemType.IMAGE;
import static com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem.ItemType.TEXT;
import static com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem.ItemType.UNKNOWN;
import static com.tevinjeffrey.vapor.okcloudapp.model.CloudAppItem.ItemType.VIDEO;
                            case R.id.nav_trash:
                                mPresenter.setNavContext(FilesActivityPresenter.NavContext.TRASH);
                                break;
                            default:
                                throw new RuntimeException("Unknown type");
                        }
                        setToolbarTitle(mPresenter.getNavContext().toString());

                        //Smoother animation when closing drawer.
                        if (oldContext != mPresenter.getNavContext()) {
                            Observable.just(new Object()).delay(250, TimeUnit.MILLISECONDS)
                                    .observeOn(AndroidSchedulers.mainThread())
                                    .subscribe(new Action1<Object>() {
                                        @Override
                                        public void call(Object o) {
                                            setupViewPager(viewPager);
                                            tabs.setupWithViewPager(viewPager);
                                        }
                                    });
                        }
                        return true;
                    }
                });

    }

    private void setToolbarTitle(CharSequence charSequence) {
        getSupportActionBar().setTitle(charSequence);
    }

    private void setupViewPager(ViewPager viewPager) {
        viewPager.setOffscreenPageLimit(5);
        Adapter adapter = new Adapter(getSupportFragmentManager());
        adapter.addFragment(FilesFragment.newInstance(ALL), "Recent");
        adapter.addFragment(FilesFragment.newInstance(IMAGE), "Images");
        adapter.addFragment(FilesFragment.newInstance(VIDEO), "Videos");
        adapter.addFragment(FilesFragment.newInstance(ARCHIVE), "Archives");
        adapter.addFragment(FilesFragment.newInstance(TEXT), "Text");
        adapter.addFragment(FilesFragment.newInstance(AUDIO), "Audio");
        adapter.addFragment(FilesFragment.newInstance(BOOKMARK), "Bookmarks");
        adapter.addFragment(FilesFragment.newInstance(UNKNOWN), "Unknown");
        viewPager.setAdapter(adapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        switch (id) {
            case android.R.id.home:
                drawerLayout.openDrawer(GravityCompat.START);
                return true;
            case R.id.action_settings:
                Intent intent = new Intent(this, SettingsActivity.class);
                startActivity(intent);
                return true;
            case R.id.action_refresh:
                mPresenter.refreshClicked();
                return true;
            case R.id.action_logout:
                new MaterialDialog.Builder(this)
                        .title(R.string.are_you_sure)
                        .positiveText(R.string.yes)
                        .negativeText(R.string.no)
                        .callback(new MaterialDialog.ButtonCallback() {
                            @Override
                            public void onPositive(MaterialDialog dialog) {
                                super.onPositive(dialog);
                                userManager.logout();
                                finish();
                                Intent intent = new Intent(FilesActivity.this, FilesActivity.class);
                                startActivity(intent);
                            }
                        })
                        .show();
                return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onItemClicked(final CloudAppItem data, View view) {
        bottomsheet.setPeekOnDismiss(true);

        if (!bottomsheet.isSheetShowing()) {
            bottomsheet.showWithSheetView(LayoutInflater.from(this).inflate(R.layout.fragment_files_info, bottomsheet, false));
            bottomsheet.setFocusableInTouchMode(true);
            bottomsheet.requestFocus();
            bottomsheet.setPeekSheetTranslation((float) (getResources().getDisplayMetrics().heightPixels * .60));
        }

        Drawable previousImage = ButterKnife.<ImageView>findById(view, R.id.files_list_image).getDrawable();

        final TextView bsFileName = ButterKnife.findById(bottomsheet, R.id.bs_file_name);
        TextView bsSizeText = ButterKnife.findById(bottomsheet, R.id.bs_size_text);
        TextView bsCreatedText = ButterKnife.findById(bottomsheet, R.id.bs_created_text);
        TextView bsViewsText = ButterKnife.findById(bottomsheet, R.id.bs_views_text);
        View downloadLink = ButterKnife.findById(bottomsheet, R.id.bs_download_link_container);
        View shareLink = ButterKnife.findById(bottomsheet, R.id.bs_share_link_container);
        View renameFile = ButterKnife.findById(bottomsheet, R.id.bs_rename_container);
        View deleteFile = ButterKnife.findById(bottomsheet, R.id.bs_delete_container);
        View expandButton = ButterKnife.findById(bottomsheet, R.id.bs_expand_icon);
        View scrim = ButterKnife.findById(bottomsheet, R.id.bs_main_scrim);
        if (data.getItemType() != IMAGE) {
            expandButton.setVisibility(View.GONE);
        }

        scrim.setBackground(ScrimUtil.makeCubicGradientScrimDrawable(
                0xaa000000, 8, Gravity.BOTTOM));
        final TouchImageView mainImage = ButterKnife.findById(bottomsheet, R.id.bs_main_icon);