ThumbnailDownloader's strange behaviour


#1

Hi everyone!

I tried to find where does the problem come from but i’m stuck, I really have no idea why it’s behaving so randomly!
It’s actually related to both chapters 26(the Challenge) and 27!

Here are my files:

PhotoGalleryFragment.java

package com.ddzsoft.photogallery.fragment;

import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ArrayAdapter;
import android.widget.GridView;
import android.widget.ImageView;

import com.ddzsoft.photogallery.FlickrFetchr;
import com.ddzsoft.photogallery.R;
import com.ddzsoft.photogallery.ThumbnailDownloader;
import com.ddzsoft.photogallery.model.GalleryItem;

import java.util.ArrayList;

/**
 * Created by Frédéric on 02/08/2014.
 */
public class PhotoGalleryFragment extends Fragment {

    private static final String TAG = "PhotoGalleryFragment";

    int mFetchedPages = 0;
    int mTotalPages = 1;

    GridView mGridView;
    ArrayList<GalleryItem> mItems;
    ThumbnailDownloader<ImageView> mThumbnailThread;

    int mCurrentScrollState;
    int mCurrentFisrtVisibleItemIndex;
    int mCurrentVisibleItemCount;


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

        setRetainInstance(true);
//        will start the AsyncTask and then fire up the background thread and call doInBackground
        new FetchItemsTask().execute();

        mThumbnailThread = new ThumbnailDownloader<ImageView>(new Handler());
        mThumbnailThread.setListener(new ThumbnailDownloader.Listener<ImageView>() {
            @Override
            public void onThumbnailDownloaded(ImageView imageView, Bitmap thumbnail) {
                if(isVisible()) {
                    imageView.setImageBitmap(thumbnail);
                }
            }
        });
        mThumbnailThread.start();
        mThumbnailThread.getLooper();
        Log.i(TAG, "Background thread started");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mThumbnailThread.quit();
        Log.i(TAG, "Background thread destroyed");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        mThumbnailThread.clearQueue();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        View v = inflater.inflate(R.layout.fragment_photo_gallery, container, false);

        mGridView = (GridView) v.findViewById(R.id.gridView);

        return v;
    }

    /* AsyncTask constructor takes in parameters:
      *  Void = Params
       * Void = Progress
       * ArrayList<GalleryItem> = Type of result */
    private class FetchItemsTask extends AsyncTask<Void, Void, ArrayList<GalleryItem>> {

        private FlickrFetchr mFlickrFetchr;

        @Override
        protected ArrayList<GalleryItem> doInBackground(Void... params) {

            mFlickrFetchr = new FlickrFetchr();
            return mFlickrFetchr.fetchItems();

        }

/*       Run after doInBackground() completes. It's run of the MAIN THREAD, so it's safe to update the UI within it*/
        @Override
        protected void onPostExecute(ArrayList<GalleryItem> items) {
            mItems = items;
            mFetchedPages++;
            mTotalPages = mFlickrFetchr.getTotalPages();

            setupAdapter();
        }

    }

    private class FetchNextPageTask extends AsyncTask<Void, Void, ArrayList<GalleryItem>> {

        @Override
        protected ArrayList<GalleryItem> doInBackground(Void... voids) {
            return new FlickrFetchr().fetchItems(mFetchedPages + 1, mItems);
        }

        @Override
        protected void onPostExecute(ArrayList<GalleryItem> items) {
            mItems = items;
            mFetchedPages++;
            ((GalleryItemAdapter) mGridView.getAdapter()).notifyDataSetChanged();

        }
    }

    private class GalleryItemAdapter extends ArrayAdapter<GalleryItem> {

        public GalleryItemAdapter(ArrayList<GalleryItem> items) {
            super(getActivity(), 0, items);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if(convertView == null){
                convertView = getActivity().getLayoutInflater().inflate(R.layout.gallery_item, parent, false);
            }

            ImageView imageView = (ImageView) convertView.findViewById(R.id.gallery_item_imageView);
            imageView.setImageResource(R.drawable.brian_up_close);
            GalleryItem item = getItem(position);
            mThumbnailThread.queueThumbnail(imageView, item.getUrl());

            return convertView;
        }
    }

    public void setupAdapter() {
        if (getActivity() != null && mGridView != null) {
            if(mItems != null){

                mGridView.setAdapter(new GalleryItemAdapter(mItems));
                mGridView.setOnScrollListener(new AbsListView.OnScrollListener() {

/*                    We need the parameters from both methods:

                        - onScrollStateChanged: scrollState
                            --> The state has to be "IDLE"! 

                        - onScroll:             firstVisibleItemIndex
                                                visibleItemCount
                                                (don't need the totalItemCount, we can get it from mItems)
                            --> To know if the last items are visible

                      I created local variables that are updated in real time
                      The job is done in a new method!!...called isGridViewEndReached()


                        */
                    @Override
                    public void onScrollStateChanged(AbsListView absListView, int scrollState) {
                        mCurrentScrollState = scrollState;
                        isGridViewEndReached();
                    }

                    @Override
                    public void onScroll(AbsListView absListView, int firstVisibleItemIndex, int visibleItemCount, int totalItemCount) {

                        mCurrentFisrtVisibleItemIndex = firstVisibleItemIndex;
                        mCurrentVisibleItemCount = visibleItemCount;

                    }
                });

            } else {
                mGridView.setAdapter(null);
            }
        }
    }

    private void isGridViewEndReached() {

//        If it's not scrolling anymore...
        if (mCurrentScrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
            Log.d(TAG, "SCROLL_STATE_IDLE");

//            ...and if the last items are visible
            if (mCurrentFisrtVisibleItemIndex + mCurrentVisibleItemCount == mItems.size()) {

                Log.d(TAG, "End of the GridView reached!");

                if (mFetchedPages < mTotalPages) {
                    Log.d(TAG, "Total pages:" + mTotalPages);

//                                We fetch the next page
                    new FetchNextPageTask().execute();

                }

            }
        }
    }

}

The ThumnailDownloader.java hasn’t been modified, should be the original code but i put it just in case I missed something!

ThumbnailDownloader.java

package com.ddzsoft.photogallery;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.util.Log;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by Frédéric on 04/08/2014.
 */
public class ThumbnailDownloader<Token> extends HandlerThread {

    private static final String TAG = "ThumbnailDownloader";
    private static final int MESSAGE_DOWNLOAD = 0;

    Handler mHandler;
    Map<Token, String> requestMap = Collections.synchronizedMap(new HashMap<Token, String>());

//    comes from the Main Thread
    Handler mResponseHandler;
    Listener<Token> mListener;

    public interface Listener<Token> {
        void onThumbnailDownloaded(Token token, Bitmap thumbnail);
    }

    public void setListener(Listener<Token> listener) {
        mListener = listener;
    }

    public ThumbnailDownloader(Handler responseHandler) {
        super(TAG);
        mResponseHandler = responseHandler;
    }

    public void queueThumbnail(Token token, String url) {
        Log.i(TAG, "Got an URL: " + url);

        requestMap.put(token, url);

        mHandler.obtainMessage(MESSAGE_DOWNLOAD, token)
                .sendToTarget();
    }

//    Called before the looper checks the queue for the first time
    @Override
    protected void onLooperPrepared() {
        mHandler = new Handler() {

//            We check the message type, retrieve the token and pass it to the handleRequest()
            @SuppressWarnings("unchecked")
            @Override
            public void handleMessage(Message msg) {
                if(msg.what == MESSAGE_DOWNLOAD){
                 Token token = (Token) msg.obj;
                    Log.i(TAG, "Got a request for url: " + requestMap.get(token));
                    handleRequest(token);
                }
            }

        };
    }

//    We get the image data here and then create a Bitmap
    private void handleRequest(final Token token) {

        try {
//            Check if the url exists
            final String url = requestMap.get(token);
            if(url != null){

                byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url);
                final Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
                Log.i(TAG, "Bitmap created");

/*                Causes the Runnable to be added to the message queue. The runnable will be run
                on the thread to which this handler is attached.*/
                mResponseHandler.post(new Runnable() {
                    @Override
                    public void run() {
//                        Check the requestMap
                        if(requestMap.get(token) == url){
                            requestMap.remove(token);
                            mListener.onThumbnailDownloaded(token, bitmap);
                        }
                    }
                });
            }
        } catch (IOException ioe) {
            Log.e(TAG, "Error downloading image", ioe);
        }

    }

    public void clearQueue() {
//        Remove any pending posts of messages with code 'what' that are in the message queue
        mHandler.removeMessages(MESSAGE_DOWNLOAD);
        requestMap.clear();
    }
}

FlickrFetchr.java

package com.ddzsoft.photogallery;

import android.net.Uri;
import android.util.Log;

import com.ddzsoft.photogallery.model.GalleryItem;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;

/**
 * Created by Frédéric on 02/08/2014.
 */
public class FlickrFetchr {

    private static final String TAG = "FlickrFetchr";

    private static final String ENDPOINT = "https://api.flickr.com/services/rest/";
    private static final String API_KEY = "#############################";
    private static final String PAGE = "page";
    private static final String METHOD_GET_RECENT = "flickr.photos.getRecent";
    private static final String PARAM_EXTRAS = "extras";
    private static final String EXTRA_SMALL_URL = "url_s";

    //    By default, only the page 1 is returned, containing 100 items "photo".
    private static final String XML_PHOTOS = "photos";
    private static final String XML_PHOTO = "photo";

    private int mTotalPages;


    public byte[] getUrlBytes(String urlSpec) throws IOException {
        URL url = new URL(urlSpec);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            /* HttpUrlConnection represents a connection, but it will not connect to the endpoint until I call
             * getInputStream(). Until then, i cannot get a valid response code! */
            InputStream in = connection.getInputStream();

            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                return null;
            }

            int bytesRead = 0;
            byte[] buffer = new byte[1024];
            while ((bytesRead = in.read(buffer)) > 0) {
                out.write(buffer, 0, bytesRead);
            }
            out.close();
            return out.toByteArray();
        } finally {
            connection.disconnect();
        }

    }

    public String getUrl(String urlSpec) throws IOException {
        return new String(getUrlBytes(urlSpec));
    }


//    Default fetchItems method, fetching the page 1 only!
    public ArrayList<GalleryItem> fetchItems() {

        ArrayList<GalleryItem> items = new ArrayList<GalleryItem>();

        try {
            /* buildUpon() returns a Uri.Builder...it allows us to build the complete URL for our
             * Flickr API Request. It's a convenience class for creating properly escaped parametized
              * URLs */
            String url = Uri.parse(ENDPOINT).buildUpon()
                    .appendQueryParameter("method", METHOD_GET_RECENT)
                    .appendQueryParameter("api_key", API_KEY)
                    .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL)
                    .build().toString();

            String xmlString = getUrl(url);
            Log.i(TAG, "Received xml: " + xmlString);
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            XmlPullParser parser = factory.newPullParser();
            parser.setInput(new StringReader(xmlString));


//            Page number initialization
            mTotalPages = getPages(parser);

//            Parse firstPage items
            parseItems(items, parser);


        } catch (IOException ioe) {
            Log.e(TAG, "Failed to fetch items", ioe);
        } catch (XmlPullParserException xppe) {
            Log.e(TAG, "Failed to parse items", xppe);
        }


        return items;
    }

//    To fetch items from the defined page number
    public ArrayList<GalleryItem> fetchItems(int page, ArrayList<GalleryItem> items) {

        try {
            /* buildUpon() returns a Uri.Builder...it allows us to build the complete URL for our
             * Flickr API Request. It's a convenience class for creating properly escaped parametized
              * URLs */
            String url = Uri.parse(ENDPOINT).buildUpon()
                    .appendQueryParameter("method", METHOD_GET_RECENT)
                    .appendQueryParameter("api_key", API_KEY)
                    .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL)
                    .appendQueryParameter(PAGE, String.valueOf(page))
                    .build().toString();

            String xmlString = getUrl(url);
            Log.i(TAG, "Received xml: " + xmlString);
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            XmlPullParser parser = factory.newPullParser();
            parser.setInput(new StringReader(xmlString));

//                mTotalPages is initialized inside
            parseItems(items, parser);

        } catch (IOException ioe) {
            Log.e(TAG, "Failed to fetch items", ioe);
        } catch (XmlPullParserException xppe) {
            Log.e(TAG, "Failed to parse items", xppe);
        }


        return items;
    }

    public void parseItems(ArrayList<GalleryItem> items, XmlPullParser parser) throws IOException, XmlPullParserException {

        int eventType = parser.next();

        while (eventType != XmlPullParser.END_DOCUMENT) {

            /* If type of event triggered is a Start Tag and that, the tag name is a "photo" then...*/
            if (eventType == XmlPullParser.START_TAG &&
                    XML_PHOTO.equals(parser.getName())) {

//                we get all the attributes
                String id = parser.getAttributeValue(null, "id");
                String caption = parser.getAttributeValue(null, "title");
                String smallUrl = parser.getAttributeValue(null, EXTRA_SMALL_URL);

//                ...create our item
                GalleryItem item = new GalleryItem();
                item.setId(id);
                item.setCaption(caption);
                item.setUrl(smallUrl);

//                ...and add it to the list
                items.add(item);
            }

//            we ask the parser to go the next event/occurrence
            eventType = parser.next();

        }

    }

    public int getPages(XmlPullParser parser) throws IOException, XmlPullParserException {

        int eventType = parser.next();
        int intPages = 0;

        while (eventType != XmlPullParser.END_DOCUMENT) {

            if (eventType == XmlPullParser.START_TAG &&
                    XML_PHOTOS.equals(parser.getName())) {

                String pages = parser.getAttributeValue(null, "pages");
                intPages = Integer.valueOf(pages);
                break;
            } else {
                eventType = parser.next();
            }

        }

        return intPages;

    }

    public int getTotalPages() {
        return mTotalPages;
    }

    public void setTotalPages(int totalPages) {
        mTotalPages = totalPages;
    }
}

Problem description:
The way I implemented the “Paging” Challenge(Chap 26) seems to work fine(well, I can’t see any problems haha) but from time to time, when the next page(next 100 items) is loaded, it’s showing the same 100 items from the 1st page! i suspect it comes from the downloader.
It’s really annoying, someone have an idea ?
My code is commented, hope it’s gonna help

Thanks in advance for your feedback


#2

Ok imagine you have just uploaded a picture of your dog to flickr
Then you immediately you start this app running and because your picture is the newest of the new it is right at the top of the list in position 1
But 20 seconds later another 125 pictures have been uploaded to flickr and your dog is in position 126
So you scroll down onto the new page and see the picture of your dog and you think to yourself "What is that doing here? That is meant to be in position one on page 1"
but that is wrong it WAS at position 1 on page 1 but you spent too long scrolling to the next page and by the time you got to the next page that image was at it’s rightful position 26 on page 2

And if you don’t harry up and get to page 3 quickly you may find the image there also

In theory it is possible if you paged at just the right time you could have EXACTLY the same images on every page