info.johannblake.shutterstockdemo.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for info.johannblake.shutterstockdemo.MainActivity.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Johann Blake
 *
 * https://www.linkedin.com/in/johannblake
 * https://plus.google.com/+JohannBlake
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package info.johannblake.shutterstockdemo;

import android.app.Activity;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.support.v4.app.FragmentActivity;
import android.support.v4.util.LruCache;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.support.v4.widget.DrawerLayout;
import android.view.ViewTreeObserver;
import android.view.animation.ScaleAnimation;
import android.widget.ImageView;
import android.support.v7.widget.RecyclerView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.support.library21.custom.SwipeRefreshLayoutBottom;

import com.google.gson.Gson;

import java.io.File;
import java.lang.ref.WeakReference;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;

import info.johannblake.widgets.jbheaderscrolllib.JBHeaderScroll;
import info.johannblake.widgets.jbprogressindicatorlib.JBProgressIndicator;

public class MainActivity extends FragmentActivity implements NavigationDrawerFragment.NavigationDrawerCallbacks {
    private static Context mContext;
    private GalleryFragment mFragmentGallery;
    private static boolean mRefreshQuery;
    private static String mQuery = "cat";
    private static boolean mAppTerminating;
    private static Thread mThreadShutterstockQuery;
    private static Thread mThreadDownloadImages;
    private static Thread mThreadLoadMissingImages;
    private static JBHeaderScroll mJBHeaderScroll;
    private DrawerLayout mDrawerLayout;

    public static MenuItem mMenuItemSearch;
    public static SearchView mSearchView;

    private final boolean INCLUDE_NAV_DRAWER = false;

    private final String APP_STATE_QUERY = "APP_STATE_QUERY";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mContext = this;
        mAppTerminating = false;

        // Create a cache to store downloaded images.
        CacheSupport.createCache(mContext);

        restoreAppState();

        NavigationDrawerFragment navigationDrawerFragment = (NavigationDrawerFragment) getSupportFragmentManager()
                .findFragmentById(R.id.navigation_drawer);

        // Set up the drawer.
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        navigationDrawerFragment.setUp(R.id.navigation_drawer, mDrawerLayout);

        if (!INCLUDE_NAV_DRAWER)
            mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        toolbar.setTitle(getString(R.string.app_name));
        toolbar.inflateMenu(R.menu.options_menu);

        mMenuItemSearch = toolbar.getMenu().findItem(R.id.search);
        mSearchView = (SearchView) mMenuItemSearch.getActionView();

        mSearchView.setOnQueryTextListener(queryTextListener);

        // Note: Calling mSearchView.setQuery doesn't set the SearchView's input field
        // probably because the SearchView's layout is inflated each time the search
        // icon is tapped. To set the input field, it has to be done after the
        // button has been tapped. Presumably, the SearchView's layout is inflated at that
        // moment.
        mSearchView.setOnSearchClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mSearchView.setQuery(mQuery, false);
            }
        });

        if (INCLUDE_NAV_DRAWER) {
            // Display the hamburger icon.
            toolbar.setNavigationIcon(R.drawable.ic_hamburger);
        }

        // Handle the user tapping on the hamburger icon.
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (INCLUDE_NAV_DRAWER)
                    mDrawerLayout.openDrawer(Gravity.START);
            }
        });

        // Move the toolbar up/down as the user scrolls the recyclerview.
        final LinearLayout llHeader = (LinearLayout) findViewById(R.id.llHeader);

        llHeader.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                final CustomRecyclerView rvImages = (CustomRecyclerView) findViewById(R.id.rvImages);
                rvImages.setOverScrollMode(ScrollView.OVER_SCROLL_NEVER);

                rvImages.setY(llHeader.getHeight());
                mJBHeaderScroll = new JBHeaderScroll(llHeader, 0);
                mJBHeaderScroll.registerScroller(rvImages, new JBHeaderScroll.IJBHeaderScroll() {
                    @Override
                    public void onResize(float top) {
                        ViewGroup.LayoutParams rlLayoutParams = new ViewGroup.LayoutParams(
                                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
                        rvImages.setLayoutParams(rlLayoutParams);
                        rvImages.setY(top);
                        llHeader.bringToFront();
                    }

                    @Override
                    public int onHeaderBeforeAnimation(boolean scrollingUp, float scrollDelta) {
                        return JBHeaderScroll.ANIMATE_HEADER_USE_DEFAULT;
                    }

                    @Override
                    public void onHeaderAfterAnimation(boolean animatedUp, float scrollDelta) {
                    }
                });

                rvImages.setJBHeaderRef(mJBHeaderScroll);
                llHeader.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
    }

    @Override
    public void onBackPressed() {
        // Close the navigation drawer if it's open.
        if (mDrawerLayout.isDrawerOpen(Gravity.START))
            mDrawerLayout.closeDrawer(Gravity.START);
        else
            super.onBackPressed();
    }

    /**
     * Used to intercept motion events needed for scrolling the toolbar up/down.
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mJBHeaderScroll != null)
            mJBHeaderScroll.onRootDispatchTouchEventListener(ev);

        return super.dispatchTouchEvent(ev);
    }

    /**
     * Captures text entered in the searchview that is used for the query.
     */
    private SearchView.OnQueryTextListener queryTextListener = new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String q) {
            // Notify threads that a new query has been entered.
            mQuery = q;
            mRefreshQuery = true;

            saveAppState();

            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {
            return false;
        }
    };

    /**
     * "Note: There's no guarantee that onSaveInstanceState() will be called before your activity is destroyed,
     * because there are cases in which it won't be necessary to save the state (such as when the user leaves
     * your activity using the Back button, because the user is explicitly closing the activity). If the system
     * calls onSaveInstanceState(), it does so before onStop() and possibly before onPause()."
     * <p/>
     * Taken from: http://developer.android.com/guide/components/activities.html
     */
    @Override
    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
        saveAppState();
    }

    @Override
    protected void onResume() {
        mAppTerminating = false;
        super.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        saveAppState();
    }

    @Override
    protected void onDestroy() {
        mAppTerminating = true;

        if (mThreadShutterstockQuery != null) {
            mThreadShutterstockQuery.interrupt();
            mThreadShutterstockQuery = null;
        }

        if (mThreadDownloadImages != null) {
            mThreadDownloadImages.interrupt();
            mThreadDownloadImages = null;
        }

        if (mThreadLoadMissingImages != null) {
            mThreadLoadMissingImages.interrupt();
            mThreadLoadMissingImages = null;
        }

        super.onDestroy();
    }

    /**
     * Saves the state of any data, so that it can be restored again when the app starts with restoreAppState.
     */
    public void saveAppState() {
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext);
        SharedPreferences.Editor editor = settings.edit();
        editor.putString(APP_STATE_QUERY, mQuery);
        editor.apply();
    }

    /**
     * Restores the state of the app that was saved when saveAppState was last called.
     */
    public void restoreAppState() {
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext);
        mQuery = settings.getString(APP_STATE_QUERY, mQuery);
    }

    @Override
    public void onNavigationDrawerItemSelected(int position) {
        if (position == 0) {
            if (mFragmentGallery == null) {
                mFragmentGallery = GalleryFragment.newInstance(position + 1);

                // update the main content by replacing fragments
                FragmentManager fragmentManager = getSupportFragmentManager();
                fragmentManager.beginTransaction().replace(R.id.container, mFragmentGallery).commit();
            }
        }
    }

    /**
     * Called by the Gallery fragment when it loads to indicate which fragment was loaded.
     */
    public void onSectionAttached(int number) {
        switch (number) {
        case 1:
            //setupRecyclerView();
            //mTitle = getString(R.string.title_section1);
            break;
        case 2:
            //mTitle = getString(R.string.title_section2);
            break;
        case 3:
            //mTitle = getString(R.string.title_section3);
            break;
        }
    }

    /**
     * Prevents the app from restarting when configuration changes occur.
     */
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
    }

    /**
     * The fragment that handles accessing Shutterstock's api and displaying images in a Recyclerview.
     */
    public static class GalleryFragment extends Fragment {
        public static ArrayList<ShutterstockResponse> mImageList = new ArrayList<>();
        public static RecyclerView mRecyclerView;

        private static RecyclerView.Adapter mAdapter;
        private static GridLayoutManager mLayoutManager;
        private JBProgressIndicator mJBProgressIndicator;
        private int mPerPage;
        private boolean mLoadMore;
        private int mTotalCols;
        private ImageView mImageViewLarge;
        private RelativeLayout mRLLargeImage;
        private RelativeLayout mRLDisabledOverlay;
        private DisplayLargeImageAsyncTask mAsyncTaskDisplayLargeImage;
        private ProgressBar mPBLoadingLarge;
        private boolean mRestartDownload;
        private boolean mAccessingShutterstock;
        private boolean mDownloadingThumbnails;
        private SwipeRefreshLayoutBottom mSRLImages;
        private LruCache<String, Bitmap> mLRUCacheRecyclerView;
        private int mScrollState = -1;
        private Bitmap mDefaultIcon;

        /**
         * The fragment argument representing the section number for this
         * fragment.
         */
        private static final String ARG_SECTION_NUMBER = "section_number";

        /**
         * Returns a new instance of this fragment for the given section
         * number.
         */
        public static GalleryFragment newInstance(int sectionNumber) {
            GalleryFragment fragment = new GalleryFragment();
            Bundle args = new Bundle();
            args.putInt(ARG_SECTION_NUMBER, sectionNumber);
            fragment.setArguments(args);
            return fragment;
        }

        public GalleryFragment() {
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, final Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main, container, false);

            mJBProgressIndicator = (JBProgressIndicator) rootView.findViewById(R.id.jbProgressIndicator);
            mRLLargeImage = (RelativeLayout) rootView.findViewById(R.id.rlLargeImage);
            mImageViewLarge = (ImageView) rootView.findViewById(R.id.ivLarge);
            mPBLoadingLarge = (ProgressBar) rootView.findViewById(R.id.pbLoadingLarge);
            mRLDisabledOverlay = (RelativeLayout) rootView.findViewById(R.id.rlDisabledOverlay);

            mDefaultIcon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_shutter);

            // Close the large image when the overlay is tapped.
            mRLDisabledOverlay.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mRLLargeImage.setVisibility(View.INVISIBLE);
                    mRLDisabledOverlay.setVisibility(View.INVISIBLE);
                }
            });

            // Calculate the maximum number of large thumbnails that can be shown on a screen. Download twice that many to
            // allow for scrolling.
            mPerPage = 50;

            // TODO: Use the full screen size for now. In a production app, get the size of the fragment.
            DisplayMetrics metrics = getActivity().getResources().getDisplayMetrics();
            int width = metrics.widthPixels;
            int height = metrics.heightPixels;

            mTotalCols = width / 150 - 1;

            if (mTotalCols < 3)
                mTotalCols = 3;

            int totalHeight = height / 150 - 1;

            mPerPage = mTotalCols * totalHeight * 2;

            // Handle the SwipeRefreshLayout's notifications.
            mSRLImages = (SwipeRefreshLayoutBottom) rootView.findViewById(R.id.srlImages);

            mSRLImages.setOnRefreshListener(new SwipeRefreshLayoutBottom.OnRefreshListener() {
                @Override
                public void onRefresh() {
                    if (mAccessingShutterstock || mDownloadingThumbnails)
                        mSRLImages.setRefreshing(false);
                    else {
                        mSRLImages.setRefreshing(true);
                        mLoadMore = true;
                    }
                }
            });

            // Create a LRU cache to hold enough images for the recyclerview.

            // The average size of a thumbnail is 5300 bytes. Allocate enough memory for one page.
            int cacheSizeImages = mPerPage * 5300;

            // Allocate no more than 1/8 of available memory.
            int cacheSizeMax = (int) Runtime.getRuntime().maxMemory() / 8;

            // If the amount of allocated memory exceeds the amount needed for caching images, use
            // the allocated amount for better performance.

            if (cacheSizeImages * 4 < cacheSizeMax)
                cacheSizeImages = cacheSizeImages * 4;

            int maxItems = cacheSizeImages / 5300;

            mLRUCacheRecyclerView = new LruCache<>(maxItems);

            // Setup the recyclerview for displaying the images
            mRecyclerView = (RecyclerView) rootView.findViewById(R.id.rvImages);

            mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    mScrollState = newState;
                }
            });

            // use this setting to improve performance if you know that changes
            // in content do not change the layout size of the RecyclerView
            mRecyclerView.setHasFixedSize(true);

            mLayoutManager = new GridLayoutManager(mContext, mTotalCols);
            mRecyclerView.setLayoutManager(mLayoutManager);

            mAdapter = new AdapterImageRecycler(mImageList);
            mRecyclerView.setAdapter(mAdapter);

            // Use a single thread to display missing images when the recyclerview stops scrolling.
            mThreadLoadMissingImages = new Thread(null, new LoadImagesOnScrollStop(),
                    "LoadImagesOnScrollStop_" + UUID.randomUUID());
            mThreadLoadMissingImages.start();

            // Use a single thread to query Shutterstock's api.
            mThreadShutterstockQuery = new Thread(null, new ShutterstockQueryRunnable(),
                    "ShutterstockQueryRunnable_" + UUID.randomUUID());
            mThreadShutterstockQuery.start();

            // Use a separate thread to download thumbnail images.
            mThreadDownloadImages = new Thread(null, new DownloadImagesRunnable(),
                    "DownloadImagesRunnable_" + UUID.randomUUID());
            mThreadDownloadImages.start();

            return rootView;

        }

        /**
         * Displays images when the recyclerview stops scrolling.
         */
        private class LoadImagesOnScrollStop implements Runnable {
            @Override
            public void run() {
                boolean itemsProcessed = false;

                do {
                    try {
                        if (mScrollState != RecyclerView.SCROLL_STATE_IDLE)
                            itemsProcessed = false;

                        if ((mScrollState == RecyclerView.SCROLL_STATE_IDLE) && !itemsProcessed) {
                            // Display only those images in the visible area that didn't load during scrolling.
                            for (int i = mLayoutManager.findFirstVisibleItemPosition(); i <= mLayoutManager
                                    .findLastVisibleItemPosition(); i++) {
                                if (mAppTerminating)
                                    return;

                                if (mScrollState != RecyclerView.SCROLL_STATE_IDLE)
                                    break;

                                ShutterstockImageInfo imageInfo = mImageList.get(
                                        getResponseIndexFromLayoutPosition(i)).data[getImageIndexFromLayoutPosition(
                                                i)];

                                if (imageInfo != null) {
                                    ShutterstockImageAsset asset = imageInfo.assets.large_thumb;

                                    if (!asset.imageLoaded) {
                                        UpdateAdapterForMissingImage updateAdapterForMissingImage = new UpdateAdapterForMissingImage(
                                                i, asset.filename);
                                        getActivity().runOnUiThread(updateAdapterForMissingImage);
                                    }
                                }
                            }

                            itemsProcessed = true;
                        }
                    } catch (Exception ex) {
                    }

                } while (!mAppTerminating);
            }
        }

        /**
         * Updates ImageViews with images.
         */
        private class UpdateAdapterForMissingImage implements Runnable {
            private int mIndex;
            private String mFilename;

            public UpdateAdapterForMissingImage(int mIndex, String filename) {
                mIndex = mIndex;
                mFilename = filename;
            }

            @Override
            public void run() {
                Thread.currentThread()
                        .setName("UpdateAdapterForMissingImage.runOnUiThread.run_" + UUID.randomUUID());

                RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(mIndex);

                if (vh == null)
                    return;

                View v = vh.itemView;
                ImageView imageView = (ImageView) v.findViewById(R.id.ivSmallThumb);

                Thread threadLoadImage = new Thread(null, new LoadImage(mFilename, imageView),
                        "LoadImage_" + UUID.randomUUID());
                threadLoadImage.start();
            }
        }

        private int getResponseIndexFromLayoutPosition(int position) {
            return position / mPerPage;
        }

        private int getImageIndexFromLayoutPosition(int position) {
            return position % mPerPage;
        }

        /**
         * The AsyncTask used to download and display the large image that the user selects (by tapping on the thumbnail).
         */
        private class DisplayLargeImageAsyncTask extends AsyncTask<Void, Void, Void> {
            public boolean mTerminate;
            private int mPosition;
            private Bitmap mBitmapLarge;
            private ShutterstockImageAsset mAsset;

            public DisplayLargeImageAsyncTask(int position) {
                mPosition = position;
            }

            @Override
            protected void onPreExecute() {
                super.onPreExecute();
                // Clear the current image and start the progess loader.
                mImageViewLarge.setImageBitmap(null);
                mPBLoadingLarge.setVisibility(View.VISIBLE);
            }

            @Override
            protected Void doInBackground(Void... params) {
                // Download the larger image.
                mAsset = mImageList
                        .get(getResponseIndexFromLayoutPosition(mPosition)).data[getImageIndexFromLayoutPosition(
                                mPosition)].assets.preview;
                File f = new File(mAsset.url);
                String filename = "preview_" + f.getName();
                filename = HTTPSupport.downloadContentToCache(mAsset.url, filename, mContext);

                if (filename != null)
                    mBitmapLarge = BitmapFactory.decodeFile(filename);

                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
                super.onPostExecute(result);

                // Display the larger image using some animation.
                if (!mTerminate) {
                    mImageViewLarge.setImageBitmap(mBitmapLarge);
                    mImageViewLarge.invalidate();
                    mPBLoadingLarge.setVisibility(View.INVISIBLE);

                    ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1, mAsset.width / 2,
                            mAsset.height / 2);
                    scaleAnimation.setDuration(300);
                    mImageViewLarge.startAnimation(scaleAnimation);
                }
            }
        }

        /**
         * Used to handle the user tapping on a thumbnail.
         */
        public interface IGalleryItemListener {
            void onClick(View v);
        }

        /**
         * The RecyclerView's mAdapter for displaying thumbnail images.
         */
        public class AdapterImageRecycler extends RecyclerView.Adapter<AdapterImageRecycler.ViewHolder> {
            private ArrayList<ShutterstockResponse> mAdapterData;

            // Provide a reference to the views for each data item
            // Complex data items may need more than one view per item, and
            // you provide access to all the views for a data item in a view holder
            public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
                public View mGalleryItemView;
                private IGalleryItemListener mIGalleryItemListener;

                public ViewHolder(View vGalleryItem, IGalleryItemListener iGalleryItemListener) {
                    super(vGalleryItem);
                    mGalleryItemView = vGalleryItem;
                    mIGalleryItemListener = iGalleryItemListener;
                    vGalleryItem.setOnClickListener(this);
                }

                @Override
                public void onClick(View v) {
                    mIGalleryItemListener.onClick(v);
                }
            }

            // The mAdapter will work with the response data returned from Shutterstock.
            public AdapterImageRecycler(ArrayList<ShutterstockResponse> adapterData) {
                mAdapterData = adapterData;
            }

            // Create new views (invoked by the layout manager)
            @Override
            public AdapterImageRecycler.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                // create a new view
                View vGalleryItem = LayoutInflater.from(parent.getContext()).inflate(R.layout.gallery_item, parent,
                        false);

                return new ViewHolder(vGalleryItem, iGalleryItemListener);
            }

            private IGalleryItemListener iGalleryItemListener = new IGalleryItemListener() {
                // Handles the user tapping on a thumbnail.
                @Override
                public void onClick(final View view) {
                    // Prevent the user from tapping on another thumbnail while a larger image is being shown.
                    if (mRLDisabledOverlay.getVisibility() == View.VISIBLE)
                        return;

                    // Display the layout for the large image.
                    mRLDisabledOverlay.setVisibility(View.VISIBLE);
                    mRLLargeImage.setVisibility(View.VISIBLE);

                    if (mAsyncTaskDisplayLargeImage != null) {
                        // mTerminate the currently running async task.
                        mAsyncTaskDisplayLargeImage.mTerminate = true;
                    }

                    // Start a new async task to download the larger image.
                    int position = (int) view.getTag();
                    mAsyncTaskDisplayLargeImage = new DisplayLargeImageAsyncTask(position);
                    mAsyncTaskDisplayLargeImage.execute();
                }
            };

            // Replace the contents of a view (invoked by the layout manager)
            @Override
            public void onBindViewHolder(ViewHolder holder, int position) {
                int respIndex = position / mPerPage;
                int imageIndex = position % mPerPage;

                // Store the mPosition of the image in the image's tag. That will be used to access
                // the data associated with the image when the user taps on the thumbnail.
                holder.mGalleryItemView.setTag(position);

                ImageView imageView = (ImageView) holder.mGalleryItemView.findViewById(R.id.ivSmallThumb);

                if (mImageList.size() > respIndex) {
                    ShutterstockImageAsset asset = mImageList.get(respIndex).data[imageIndex].assets.large_thumb;

                    Bitmap bm = mLRUCacheRecyclerView.get(asset.url);

                    if (bm != null) {
                        imageView.setImageBitmap(bm);
                        asset.imageLoaded = true;
                    } else {
                        asset.imageLoaded = false;

                        if (mScrollState == RecyclerView.SCROLL_STATE_IDLE) {
                            createImageFrame(holder.mGalleryItemView, asset.width, asset.height);

                            if (asset.filename != null) {
                                // Load the image from a separate thread.
                                Thread threadLoadImage = new Thread(null, new LoadImage(asset.filename, imageView),
                                        "LoadImage_" + UUID.randomUUID());
                                threadLoadImage.start();
                            } else {
                                imageView.setImageBitmap(mDefaultIcon);
                                createImageFrame(holder.mGalleryItemView, asset.width, asset.height);
                            }
                        } else {
                            imageView.setImageBitmap(mDefaultIcon);
                            createImageFrame(holder.mGalleryItemView, asset.width, asset.height);
                        }
                    }
                } else
                    imageView.setImageBitmap(null);
            }

            /**
             * ImageViews are inside of a LinearLayout. This will resize the LinearLayout to be the size of the image.
             *
             * @param v The LinearLayout that is the parent of the ImageView.
             * @param w The width of the image.
             * @param h The heihgt of the image.
             */
            private void createImageFrame(View v, int w, int h) {
                ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(w, h);
                v.setLayoutParams(layoutParams);
            }

            // Return the size of your dataset (invoked by the layout manager)
            @Override
            public int getItemCount() {
                return mAdapterData.size() * mPerPage;
            }
        }

        /**
         * Loads an image in the background.
         */
        private class LoadImage implements Runnable {
            private final String mFilename;
            private final WeakReference<ImageView> imageViewReference;

            public LoadImage(String filename, ImageView imageView) {
                mFilename = filename;
                imageViewReference = new WeakReference<>(imageView);
            }

            @Override
            public void run() {
                // Check if image exists in the cache.
                String cacheFilename = CacheSupport.fileExistsInCache(mFilename, mContext);

                if (cacheFilename == null)
                    return;

                // Load the image.
                final Bitmap bm = BitmapFactory.decodeFile(cacheFilename);
                cacheBitmapInLRU(mFilename, bm);

                getActivity().runOnUiThread(new Runnable() {
                    public void run() {
                        Thread.currentThread().setName("LoadImage.runOnUiThread.run_" + UUID.randomUUID());

                        final ImageView imageView = imageViewReference.get();

                        if (imageView != null)
                            imageView.setImageBitmap(bm);
                    }
                });
            }
        }

        /**
         * Stores a bitmap in the LRU cache.
         *
         * @param filename The mFilename of the bitmap. It will be the key in the cache.
         * @param bm       The bitmap to store.
         */
        private synchronized void cacheBitmapInLRU(String filename, Bitmap bm) {
            mLRUCacheRecyclerView.put(filename, bm);
        }

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            ((MainActivity) activity).onSectionAttached(getArguments().getInt(ARG_SECTION_NUMBER));
        }

        /**
         * The runnable used to query Shutterstock's api. For performance reasons, this runnable
         * should only ever be launched once. It remains alive for the duration of the app but
         * only accesses Shutterstock's api when new data is needed.
         */
        class ShutterstockQueryRunnable implements Runnable {
            @Override
            public void run() {
                try {
                    int pageNum = 1;

                    Thread.currentThread().setName("MainActivity.ShutterstockQueryRunnable_" + UUID.randomUUID());

                    // Create an object to store the request parameters.
                    HttpResp httpResp = new HttpResp();

                    // Convert the object to JSON, which gets plugged into the request body.
                    Gson gson = new Gson();

                    // TODO: encrypt the authorization key to make it harder for hackers to extract.
                    // NOTE: Shutterstock's documentation indicates that you combine your client key with secret and encode it with Base64.
                    // What the docs don't mention is that you have to separate the two with a colon before encoding. Ex.: 1234:4321
                    // You can encode your own authorization key at: https://www.base64decode.org/
                    HashMap<String, String> hmHeaders = new HashMap<>();
                    hmHeaders.put("Authorization",
                            "Basic MjU3NzRhYTUyMmM5YWMzZmJlMGY6MDc2ZWNkZWUwZmEyOTU2MDBiMDVhYjVjMjBjNTIzMTU2ZTRhOGIzZQ==");

                    do {
                        // For a better performance, limit the amount of data returned by requesting only the description, small thumbnail and preview.
                        // TODO: It should be possible to limit the assets field to just small_thumb and preview but haven't been able to figure out how. Currently large_thumb is also included
                        // in the download.
                        String url = "https://api.shutterstock.com/v2/images/search?query="
                                + URLEncoder.encode(mQuery, "UTF-8")
                                + "&fields=page,per_page,total_count,data(id,description,assets)&page=" + pageNum
                                + "&per_page=" + mPerPage;

                        mAccessingShutterstock = true;
                        getActivity().runOnUiThread(runnableShowProgressIndicator);

                        if (HTTPSupport.doHttpGET(url, hmHeaders, httpResp)) {
                            // Deserialize the result and cache it.
                            ShutterstockResponse response = gson.fromJson(httpResp.Data,
                                    ShutterstockResponse.class);
                            mImageList.add(response);
                        }

                        mAccessingShutterstock = false;

                        if (!mDownloadingThumbnails)
                            getActivity().runOnUiThread(runnableHideProgressIndicator);

                        // Wait until a request for new or more data is issued.

                        do {
                            Thread.sleep(100);

                            if (mAppTerminating)
                                return;

                        } while (!mLoadMore && !mRefreshQuery);

                        mLoadMore = false;

                        if (mRefreshQuery) {
                            // Entering a new query will delete all the existing data.
                            mImageList.clear();
                            pageNum = 1;
                            mRestartDownload = true;
                            mRefreshQuery = false;
                        } else
                            pageNum++;

                    } while (!mAppTerminating);
                } catch (Exception ex) {
                    Logger.Log(getActivity(), ex, Log.ERROR);
                }
            }
        }

        /**
         * Handles the downloading of thumbnail images. This runnable should only be created once and live for
         * the duration of the app.
         */
        class DownloadImagesRunnable implements Runnable {
            @Override
            public void run() {
                int startIndex = 0;

                do {
                    boolean imagesAdded = false;
                    int col = 1;

                    mDownloadingThumbnails = true;
                    getActivity().runOnUiThread(runnableShowProgressIndicator);

                    // Process each response starting after the last set that was processed.
                    for (; startIndex < mImageList.size(); startIndex++) {
                        ShutterstockResponse response = mImageList.get(startIndex);

                        // Process each image in the list.
                        for (ShutterstockImageInfo imageInfo : response.data) {
                            if (mAppTerminating)
                                return;

                            // Get just the mFilename of the image without path.
                            File f = new File(imageInfo.assets.large_thumb.url);
                            imageInfo.assets.large_thumb.filename = f.getName();
                            String cacheFilename = CacheSupport
                                    .fileExistsInCache(imageInfo.assets.large_thumb.filename, mContext);

                            // If the file exists in cache, load it from there, otherwise download it.
                            if (cacheFilename != null)
                                cacheBitmapInLRU(imageInfo.assets.large_thumb.url,
                                        BitmapFactory.decodeFile(cacheFilename));
                            else {
                                String filenameWithPath = HTTPSupport.downloadContentToCache(
                                        imageInfo.assets.large_thumb.url, imageInfo.assets.large_thumb.filename,
                                        mContext);
                                cacheBitmapInLRU(imageInfo.assets.large_thumb.url,
                                        BitmapFactory.decodeFile(filenameWithPath));
                            }

                            // For performance reasons, only update the mAdapter after a row has been added.
                            if (col == mTotalCols) {
                                col = 1;
                                getActivity().runOnUiThread(runnableUpdateRecycler);
                            } else
                                col++;

                            imagesAdded = true;
                        }
                    }

                    mDownloadingThumbnails = false;

                    getActivity().runOnUiThread(runnableHideRefresher);

                    if (!mAccessingShutterstock)
                        getActivity().runOnUiThread(runnableHideProgressIndicator);

                    if (imagesAdded)
                        getActivity().runOnUiThread(runnableUpdateRecycler);

                    // Check periodically if any new responses have been received.
                    do {
                        try {
                            Thread.sleep(100);
                        } catch (Exception ex) {
                        }

                        if (mAppTerminating)
                            return;

                        if (mRestartDownload)
                            startIndex = 0;

                        if (mImageList.size() > startIndex)
                            break;
                    } while (true);

                    mRestartDownload = false;

                } while (!mAppTerminating);
            }
        }

        /**
         * Hides the animated refresher..
         */
        private Runnable runnableHideRefresher = new Runnable() {
            @Override
            public void run() {
                mSRLImages.setRefreshing(false);
            }
        };

        /**
         * Notifies the RecyclerView to update it's UI.
         */
        private Runnable runnableUpdateRecycler = new Runnable() {
            @Override
            public void run() {
                mAdapter.notifyDataSetChanged();
            }
        };

        /**
         * Shows the horizontal progress indicator.
         */
        private Runnable runnableShowProgressIndicator = new Runnable() {
            @Override
            public void run() {
                mJBProgressIndicator.showHide(true);
            }
        };

        /**
         * Hides the horizontal progress indicator.
         */
        private Runnable runnableHideProgressIndicator = new Runnable() {
            @Override
            public void run() {
                mJBProgressIndicator.showHide(false);
            }
        };
    }
}