Java tutorial
/* * 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); } }; } }