com.antew.redditinpictures.library.ui.ImageViewerFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.antew.redditinpictures.library.ui.ImageViewerFragment.java

Source

/*
 * Copyright (C) 2014 Antew
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.antew.redditinpictures.library.ui;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import butterknife.ButterKnife;
import butterknife.InjectView;
import com.antew.redditinpictures.device.ScreenSize;
import com.antew.redditinpictures.library.Constants;
import com.antew.redditinpictures.library.image.Image;
import com.antew.redditinpictures.library.image.ImageResolver;
import com.antew.redditinpictures.library.imgur.ImgurAlbumApi.Album;
import com.antew.redditinpictures.library.interfaces.SystemUiStateProvider;
import com.antew.redditinpictures.library.model.ImageSize;
import com.antew.redditinpictures.library.model.reddit.PostData;
import com.antew.redditinpictures.library.preferences.SharedPreferencesHelper;
import com.antew.redditinpictures.library.ui.base.BaseFragment;
import com.antew.redditinpictures.library.util.ImageDownloader;
import com.antew.redditinpictures.library.util.ImageUtil;
import com.antew.redditinpictures.library.util.Ln;
import com.antew.redditinpictures.library.util.Strings;
import com.antew.redditinpictures.pro.R;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import javax.inject.Inject;
import uk.co.senab.photoview.PhotoView;
import uk.co.senab.photoview.PhotoViewAttacher.OnPhotoTapListener;

/**
 * This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}.
 */
public abstract class ImageViewerFragment extends BaseFragment {
    protected static final String IMAGE_DATA_EXTRA = "extra_image_data";
    protected static final String IMAGE_ALBUM_EXTRA = "extra_image_album";
    /**
     * Movement threshold used to decide whether to cancel the toggle
     * between windowed mode and fullscreen mode in the
     * WebView in {@link #getWebViewOnTouchListener()}
     */
    private static float MOVE_THRESHOLD;
    private final Object mPauseWorkLock = new Object();
    protected PostData mImage;
    @InjectView(R.id.iv_imageView)
    protected ImageView mImageView;
    protected WebView mWebView;
    protected boolean mWebViewInitialized = false;
    protected boolean mPauseWork = false;
    protected String mResolvedImageUrl = null;
    protected Image mResolvedImage = null;
    protected int mActionBarHeight;
    protected Album mAlbum;
    protected AsyncTask<String, Void, Image> mResolveImageTask = null;
    protected SystemUiStateProvider mSystemUiStateProvider;
    protected boolean mFragmentVisibleToUser = false;
    protected boolean mShowHighQualityImages = false;
    @InjectView(R.id.pb_progress)
    ProgressBar mProgress;
    @InjectView(R.id.rl_post_information_wrapper)
    RelativeLayout mPostInformationWrapper;
    @InjectView(R.id.tv_post_title)
    TextView mPostTitle;
    @InjectView(R.id.tv_post_information)
    TextView mPostInformation;
    @InjectView(R.id.btn_view_gallery)
    Button mBtnViewGallery;
    @InjectView(R.id.webview_stub)
    ViewStub mViewStub;
    @InjectView(R.id.tv_post_votes)
    TextView mPostVotes;
    /**
     * This BroadcastReceiver handles updating the score when a vote is cast or changed
     */
    private BroadcastReceiver mScoreUpdateReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.hasExtra(Constants.Extra.EXTRA_PERMALINK) && intent.hasExtra(Constants.Extra.EXTRA_SCORE)
                    && mImage != null
                    && mImage.getPermalink().equals(intent.getStringExtra(Constants.Extra.EXTRA_PERMALINK))) {

                if (mPostVotes != null) {
                    mPostVotes.setText("" + intent.getIntExtra(Constants.Extra.EXTRA_SCORE, 0));
                }
            }
        }
    };
    @InjectView(R.id.b_retry)
    Button mRetry;
    @Inject
    ImageDownloader mImageDownloader;
    @InjectView(R.id.tv_error_message)
    TextView mErrorMessage;
    @Inject
    ScreenSize mScreenSize;
    private boolean mExitTasksEarly = false;
    private boolean mCancelClick = false;
    private float mDownXPos = 0;
    private float mDownYPos = 0;
    private BroadcastReceiver mToggleFullscreenIntent = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            boolean isSystemUiVisible = intent.getBooleanExtra(Constants.Extra.EXTRA_IS_SYSTEM_UI_VISIBLE, false);
            if (isSystemUiVisible) {
                hidePostDetails();
            } else {
                showPostDetails();
            }
        }
    };

    /**
     * Empty constructor as per the Fragment documentation
     */
    public ImageViewerFragment() {
    }

    /**
     * Set a hint to the system about whether this fragment's UI is currently visible
     * to the user. This hint defaults to true and is persistent across fragment instance
     * state save and restore.
     * <p/>
     * <p>An app may set this to false to indicate that the fragment's UI is
     * scrolled out of visibility or is otherwise not directly visible to the user.
     * This may be used by the system to prioritize operations such as fragment lifecycle updates
     * or loader ordering behavior.</p>
     *
     * @param isVisibleToUser
     *     true if this fragment's UI is currently visible to the user (default),
     *     false if it is not.
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        mFragmentVisibleToUser = isVisibleToUser;

        // If we have a webview shown we want to hide it when the fragment is not being viewed to prevent the lag from swiping between fragments.
        if (mWebView != null) {
            if (mFragmentVisibleToUser) {
                mWebView.setVisibility(View.VISIBLE);
            } else {
                mWebView.setVisibility(View.GONE);
                // Set the view to uninitialized since it could get GC'd and we want to show the progress bar when/if they navigate back.
                mWebViewInitialized = false;
            }
        }
    }

    /**
     * Populate image using a url from extras.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        loadExtras();
    }

    public void loadExtras() {
        mImage = getArguments() != null ? (PostData) getArguments().getParcelable(IMAGE_DATA_EXTRA) : null;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View view = inflater.inflate(R.layout.image_detail_fragment, container, false);
        ButterKnife.inject(this, view);

        mRetry.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                resolveImage();
            }
        });

        return view;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        if (mImage != null) {
            outState.putParcelable(IMAGE_DATA_EXTRA, mImage);
        }

        if (mAlbum != null) {
            outState.putParcelable(IMAGE_ALBUM_EXTRA, mAlbum);
        }

        super.onSaveInstanceState(outState);
    }

    @Override
    public void onDestroy() {
        LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mScoreUpdateReceiver);
        LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mToggleFullscreenIntent);

        if (mImageView != null) {
            mImageView.setImageDrawable(null);
        }

        if (mWebView != null) {
            mWebView.destroy();
        }

        if (mResolveImageTask != null) {
            if (mResolveImageTask.getStatus() != AsyncTask.Status.FINISHED) {
                Ln.i("onDestroy - Cancelling resolveImageTask");
                mResolveImageTask.cancel(true);
            }
        }

        super.onDestroy();
    }

    protected abstract void resolveImage();

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        populatePostData(mPostInformationWrapper);

        if (savedInstanceState != null) {
            loadSavedInstanceState(savedInstanceState);
        }

        final Activity act = getActivity();

        LocalBroadcastManager.getInstance(act).registerReceiver(mScoreUpdateReceiver,
                new IntentFilter(Constants.Broadcast.BROADCAST_UPDATE_SCORE));
        LocalBroadcastManager.getInstance(act).registerReceiver(mToggleFullscreenIntent,
                new IntentFilter(Constants.Broadcast.BROADCAST_TOGGLE_FULLSCREEN));

        // Set up on our tap listener for the PhotoView which we use to toggle between fullscreen
        // and windowed mode
        ((PhotoView) mImageView).setOnPhotoTapListener(getOnPhotoTapListener(act));

        MOVE_THRESHOLD = 20 * getResources().getDisplayMetrics().density;

        // Calculate ActionBar height
        TypedValue tv = new TypedValue();
        if (getActivity().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
            mActionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data,
                    getActivity().getResources().getDisplayMetrics());
        }

        try {
            mSystemUiStateProvider = (SystemUiStateProvider) getActivity();
        } catch (ClassCastException e) {
            Ln.e(e, "The activity must implement the SystemUiStateProvider interface");
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        if (mSystemUiStateProvider.isSystemUiVisible()) {
            showPostDetails();
        } else {
            hidePostDetails();
        }

        mShowHighQualityImages = SharedPreferencesHelper.getShowHighQualityImages(getActivity());

        // Use the parent activity to load the image asynchronously into the ImageView (so a single
        // cache can be used over all pages in the ViewPager
        if (ImageViewerActivity.class.isInstance(getActivity())) {
            resolveImage();
        }
    }

    public void showPostDetails() {
        if (shouldShowPostInformation()) {
            mPostInformationWrapper.setVisibility(View.VISIBLE);
            mPostInformationWrapper.animate().setDuration(500).y(mActionBarHeight);
        }
    }

    public void hidePostDetails() {
        mPostInformationWrapper.animate().setDuration(500).y(-400);
    }

    protected abstract boolean shouldShowPostInformation();

    protected abstract void populatePostData(View v);

    protected void loadSavedInstanceState(Bundle savedInstanceState) {
        if (savedInstanceState.containsKey(IMAGE_DATA_EXTRA)) {
            mImage = savedInstanceState.getParcelable(IMAGE_DATA_EXTRA);
        }

        if (savedInstanceState.containsKey(IMAGE_ALBUM_EXTRA)) {
            mAlbum = savedInstanceState.getParcelable(IMAGE_ALBUM_EXTRA);
        }
    }

    private OnPhotoTapListener getOnPhotoTapListener(final Activity activity) {
        return new OnPhotoTapListener() {

            @Override
            public void onPhotoTap(View view, float x, float y) {
                Intent intent = new Intent(Constants.Broadcast.BROADCAST_TOGGLE_FULLSCREEN);
                intent.putExtra(Constants.Extra.EXTRA_IS_SYSTEM_UI_VISIBLE,
                        mSystemUiStateProvider.isSystemUiVisible());
                LocalBroadcastManager.getInstance(activity).sendBroadcast(intent);
            }
        };
    }

    public void loadImage(Image image) {
        if (image == null) {
            Ln.e("Received null url in loadImage(String imageUrl)");
            showImageError();
            return;
        }

        try {
            mResolvedImage = image;
            mResolvedImageUrl = mResolvedImage.getSize(ImageSize.LARGE_THUMBNAIL);

            // Falllback to the Original if we can't resolve.
            if (Strings.isEmpty(mResolvedImageUrl)) {
                mResolvedImageUrl = mResolvedImage.getSize(ImageSize.ORIGINAL);
            }

            if (mShowHighQualityImages) {
                mResolvedImageUrl = mResolvedImage.getSize(ImageSize.ORIGINAL);
            }

            // Fallback to the URL if we can't resolve.
            if (Strings.isEmpty(mResolvedImageUrl)) {
                mResolvedImageUrl = mResolvedImage.getUrl();
            }

            if (ImageUtil.isGif(mResolvedImageUrl)) {
                Picasso.with(getActivity()).load(R.drawable.loading_spinner_76).into(mImageView);
                loadGifInWebView(mResolvedImageUrl);
            } else {
                if (mShowHighQualityImages) {
                    Picasso.with(getActivity()).load(Uri.parse(mResolvedImageUrl)).into(mImageView, new Callback() {
                        @Override
                        public void onSuccess() {
                            hideProgress();
                        }

                        @Override
                        public void onError() {
                            showImageError();
                        }
                    });
                    mResolvedImageUrl = mResolvedImage.getSize(ImageSize.ORIGINAL);
                } else {
                    Picasso.with(getActivity()).load(Uri.parse(mResolvedImageUrl))
                            .resize(mScreenSize.getWidth(), mScreenSize.getHeight()).centerInside()
                            .into(mImageView, new Callback() {
                                @Override
                                public void onSuccess() {
                                    hideProgress();
                                }

                                @Override
                                public void onError() {
                                    showImageError();
                                }
                            });
                }
            }
        } catch (Exception e) {
            Ln.e(e, "Failed to load image");
            showImageError();
        }
    }

    protected void showImageError() {
        hideProgress();
        if (mErrorMessage != null) {
            mErrorMessage.setVisibility(View.VISIBLE);
        }
        if (mRetry != null) {
            mRetry.setVisibility(View.VISIBLE);
        }
    }

    public void loadGifInWebView(final String imageUrl) {
        if (mViewStub.getParent() != null) {
            mWebView = (WebView) mViewStub.inflate();
        }

        initializeWebView();
        /**
         * On earlier version of Android, {@link android.webkit.WebView#loadData(String, String, String)} decides to just show the HTML instead of actually display it.
         *
         * So, for older version we make it load from a base URL, which fixes it for some reason...
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                // When the current view isn't being directly viewed by the user we want to defer it slightly so that the one currently being displayed can have priority.
                if (!mFragmentVisibleToUser) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        Ln.e(e);
                    }
                }
                mWebView.post(new Runnable() {
                    @Override
                    public void run() {
                        if (imageUrl != null) {
                            mWebView.loadData(getHtmlForImageDisplay(imageUrl), "text/html", "utf-8");
                            mImageView.setVisibility(View.GONE);
                        } else {
                            mWebView.destroy();
                            showImageError();
                        }
                    }
                });
            }
        }).start();
    }

    protected void hideProgress() {
        if (mProgress != null) {
            mProgress.setVisibility(View.GONE);
        }
    }

    public void initializeWebView() {
        assert mWebView != null : "WebView should not be null!";

        WebSettings settings = mWebView.getSettings();
        settings.setSupportZoom(true);
        settings.setBuiltInZoomControls(true);
        settings.setLoadWithOverviewMode(true);
        settings.setUseWideViewPort(true);
        settings.setDisplayZoomControls(false);
        // Before loading the actual content, let's let the WebView initialize everything.
        mWebView.loadData("<html></html>", "text/html", "utf-8");

        mWebView.setBackgroundColor(Color.BLACK);
        mWebView.setWebViewClient(new WebViewClient() {
            /**
             * Notify the host application that a page has finished loading. This method
             * is called only for main frame. When onPageFinished() is called, the
             * rendering picture may not be updated yet. To get the notification for the
             * new Picture, use {@link android.webkit.WebView.PictureListener#onNewPicture}.
             *
             * @param view
             *     The WebView that is initiating the callback.
             * @param url
             *     The url of the page.
             */
            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                // We first initialize the webview with mostly blank HTML content, so don't want to show it until we get to the image.
                if (mWebView != null && mWebViewInitialized) {
                    mWebView.setVisibility(View.VISIBLE);
                } else if (!mWebViewInitialized) {
                    mWebViewInitialized = true;
                }
            }
        });
        mWebView.setOnTouchListener(getWebViewOnTouchListener());
    }

    public String getHtmlForImageDisplay(String imageUrl) {
        return Constants.WEBVIEW_IMAGE_HTML_BEGIN + imageUrl + Constants.WEBVIEW_IMAGE_HTML_END;
    }

    /**
     * This handles receiving the touch events in the WebView so that we can
     * toggle between fullscreen and windowed mode.
     * <p/>
     * The first time the user touches the screen we save the X and Y coordinates.
     * If we receive a {@link MotionEvent#ACTION_DOWN} event we compare the previous
     * X and Y coordinates to the saved coordinates, if they are greater than {@link
     * #MOVE_THRESHOLD}
     * we prevent the toggle from windowed mode to fullscreen mode or vice versa, the idea
     * being that the user is either dragging the image or using pinch-to-zoom.
     * <p/>
     * TODO: Implement handling for double tap to zoom.
     *
     * @return The {@link OnTouchListener} for the {@link WebView} to use.
     */
    public OnTouchListener getWebViewOnTouchListener() {
        return new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mCancelClick = false;
                    mDownXPos = event.getX();
                    mDownYPos = event.getY();
                    break;
                case MotionEvent.ACTION_UP:
                    if (!mCancelClick) {
                        Intent intent = new Intent(Constants.Broadcast.BROADCAST_TOGGLE_FULLSCREEN);
                        intent.putExtra(Constants.Extra.EXTRA_IS_SYSTEM_UI_VISIBLE,
                                mSystemUiStateProvider.isSystemUiVisible());
                        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent);
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (Math.abs(event.getX() - mDownXPos) > MOVE_THRESHOLD
                            || Math.abs(event.getY() - mDownYPos) > MOVE_THRESHOLD) {
                        mCancelClick = true;
                    }
                    break;
                }

                // Return false so that we still let the WebView consume the event
                return false;
            }
        };
    }

    protected void showProgress() {
        if (mProgress != null) {
            mProgress.setVisibility(View.VISIBLE);
        }
    }

    /**
     * The actual AsyncTask that will asynchronously resolve the URL we should display.
     */
    class ResolveImageTask extends AsyncTask<String, Void, Image> {
        private String data;

        public ResolveImageTask() {
        }

        /**
         * Background processing.
         */
        @Override
        protected Image doInBackground(String... params) {
            data = params[0];
            Image resolvedImage = null;
            Ln.i("Resolving Image Url: %s", data);
            // Wait here if work is paused and the task is not cancelled
            synchronized (mPauseWorkLock) {
                while (mPauseWork && !isCancelled()) {
                    try {
                        mPauseWorkLock.wait();
                    } catch (InterruptedException e) {
                    }
                }
            }

            if (!isCancelled() && !mExitTasksEarly) {
                resolvedImage = ImageResolver.resolve(data);
            }

            return resolvedImage;
        }

        /**
         * Once we're resolved the URL, pass it to our load function to download/display it
         */
        @Override
        protected void onPostExecute(Image image) {
            // if cancel was called on this task or the "exit early" flag is set then we're done
            if (isCancelled() || mExitTasksEarly) {
                image = null;
            }

            loadImage(image);
        }
    }
}