<|start_filename|>src/com/donnemartin/android/photogallery/StartupReceiver.java<|end_filename|>
package com.donnemartin.android.photogallery;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

public class StartupReceiver extends BroadcastReceiver {
    // When this BroadcastReceiver wakes up, it will call onReceive then die.
    // Because it is short lived, you cannot use async APIs or register any
    // listeners because the receiver will not be alive longer than the length
    // of onReceive.  onReceive runs on the main thread, so no networking or
    // work with permanent storage.
    // BroadcastReceivers are useful for small plumbing code.

    private static final String TAG = "StartupReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i(TAG, "Received broadcast intent: " + intent.getAction());

        SharedPreferences prefs =
            PreferenceManager.getDefaultSharedPreferences(context);
        boolean isOn = prefs.getBoolean(PollService.PREF_IS_ALARM_ON, false);
        PollService.setServiceAlarm(context, isOn);
    }
}


<|start_filename|>src/com/donnemartin/android/photogallery/PhotoPageFragment.java<|end_filename|>
package com.donnemartin.android.photogallery;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.TextView;

public class PhotoPageFragment extends VisibleFragment {

    private String mUrl;
    @SuppressWarnings("FieldCanBeLocal")
    private WebView mWebView;
    private static final int MAX_PROGRESS = 100;

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

        mUrl = getActivity().getIntent().getData().toString();
    }

    // Flickr requires JavaScript to work if using a WebView
    @SuppressLint("SetJavaScriptEnabled")
    @Override
    public View onCreateView(LayoutInflater inflater,
                             ViewGroup parent,
                             Bundle savedInstanceState) {
        View view =
            inflater.inflate(R.layout.fragment_photo_page, parent, false);

        final ProgressBar progressBar =
            (ProgressBar)view.findViewById(R.id.progressBar);
        progressBar.setMax(MAX_PROGRESS);
        final TextView titleTextView =
            (TextView)view.findViewById(R.id.titleTextView);

        mWebView = (WebView)view.findViewById(R.id.webView);

        // Flickr requirement to enable JavaScript
        mWebView.getSettings().setJavaScriptEnabled(true);

        // WebClientView allows you to respond to rendering events like detect
        // when rendering starts or decide whether to submit a POST request
        // to the server.
        mWebView.setWebViewClient(new WebViewClient() {
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // Tell WebViewClient to go ahead and load the URL, we are not
                // doing anything custom with it.
                return false;
            }
        });
        mWebView.setWebChromeClient(new WebChromeClient() {
            public void onProgressChanged(WebView webView, int progress) {
                if (progress == MAX_PROGRESS) {
                    progressBar.setVisibility(View.INVISIBLE);
                } else {
                    progressBar.setVisibility(View.VISIBLE);
                    progressBar.setProgress(progress);
                }
            }

            public void onReceivedTitle(WebView webView, String title) {
                titleTextView.setText(title);
            }
        });

        // Needs to be done after configuring the WebView
        mWebView.loadUrl(mUrl);

        return view;
    }
}


<|start_filename|>src/com/donnemartin/android/photogallery/PollService.java<|end_filename|>
package com.donnemartin.android.photogallery;

import java.util.ArrayList;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

// A service's intents are called commands, which are serviced off a queue
// When it receives its first command, the IntentService starts up, fires up
// a background thread, and puts the command on a queue.  It then services
// each command in order, calling onHandleIntent on its background thread
// for each command.  The service is destroyed when there are no commands left.
public class PollService extends IntentService {

    private static final String TAG = "PollService";

    private static final int MS_PER_SEC = 1000;
    private static final int SEC_PER_MIN = 1;
    private static final int POLL_INTERVAL = MS_PER_SEC * SEC_PER_MIN * 5;
    public static final String PREF_IS_ALARM_ON = "isAlarmOn";

    // Send the broadcast notification only when the application is closed
    public static final String ACTION_SHOW_NOTIFICATION =
        "com.donnemartin.android.photogallery.SHOW_NOTIFICATION";

    // Ensure we are the only app that can trigger VisibleFragment's receiver
    public static final String PERM_PRIVATE =
        "com.donnemartin.android.photogallery.PRIVATE";

    public PollService() {
        super(TAG);
    }

    @Override
    public void onHandleIntent(Intent intent) {
        // Since Android provides the ability for a user to turn off networking
        // for background apps, we need to verify this is available
        ConnectivityManager cm = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);

        @SuppressWarnings("deprecation")
        // Pre ICS, if you did not check for getBackgroundDataSetting, you
        // could still use the network.  Post ICS, the background data setting
        // for users disables networking entirely, which requires you to check
        // getActiveNetworkInfo.  getActiveNetworkInfo requires
        // ACCESS_NETWORK_STATE in the manifest
        boolean isNetworkAvailable = cm.getBackgroundDataSetting() &&
            cm.getActiveNetworkInfo() != null;

        if (isNetworkAvailable) {
            // Pull out the current query and the last result ID from the
            // default SharedPreferences
            SharedPreferences prefs =
                PreferenceManager.getDefaultSharedPreferences(this);
            String query = prefs.getString(FlickrConn.PREF_SEARCH_QUERY, null);
            String lastResultId =
                prefs.getString(FlickrConn.PREF_LAST_RESULT_ID, null);

            ArrayList<GalleryItem> items;

            // Fetch the latest result set
            if (query != null) {
                items = new FlickrConn().search(query);
            }
            else {
                items = new FlickrConn().fetchItems();
            }

            if (items.size() > 0) {
                // Grab the first result
                String resultId = items.get(0).getId();

                if (!resultId.equals(lastResultId)) {
                    Log.i(TAG, "Got a new result: " + resultId);

                    Resources res = getResources();
                    PendingIntent pi = PendingIntent
                        .getActivity(
                            this,
                            0,
                            new Intent(this, PhotoGalleryActivity.class),
                            0);

                    Notification notification =
                        new NotificationCompat.Builder(this)
                        .setTicker(res.getString(R.string.new_pictures_title))
                        .setSmallIcon(android.R.drawable.ic_menu_report_image)
                        .setContentTitle(
                            res.getString(R.string.new_pictures_title))
                        .setContentText(
                            res.getString(R.string.new_pictures_text))
                        .setContentIntent(pi)
                        .setAutoCancel(true)
                        .build();

                    showBackgroundNotification(0, notification);
                }

                // Store the first result back in SharedPreferences
                prefs.edit()
                        .putString(FlickrConn.PREF_LAST_RESULT_ID, resultId)
                        .commit();
            }
        }
    }

    // Static method to keep the alarm code with other code in PollService that
    // it is related to and allows other components to invoke it.  Usually
    // you want to turn an alarm on and off from front end code in a fragment
    // or other controller.
    public static void setServiceAlarm(Context context, boolean isOn) {
        // Construct PendingIntent that starts PollService
        // getService packages up an invocation of Context.startService
        Intent intent = new Intent(context, PollService.class);
        PendingIntent pi = PendingIntent.getService(context, 0, intent, 0);

        // AlarmManager will keep firing intents to start PollService over and
        // over even if the app is exited
        AlarmManager alarmManager = (AlarmManager)
            context.getSystemService(Context.ALARM_SERVICE);

        if (isOn) {
            // Set the alarm
            alarmManager.setRepeating(AlarmManager.RTC,
                                      System.currentTimeMillis(),
                                      POLL_INTERVAL,
                                      pi);
        } else {
            // Can only register one alarm for each PendingIntent
            alarmManager.cancel(pi);
            pi.cancel();
        }

        // Set alarm flag that the StartupReceiver can use to turn the alarm
        // on or off at boot
        PreferenceManager.getDefaultSharedPreferences(context)
            .edit()
            .putBoolean(PollService.PREF_IS_ALARM_ON, isOn)
            .commit();
    }

    public static boolean isServiceAlarmOn(Context context) {
        Intent intent = new Intent(context, PollService.class);
        // FLAG_NO_CREATE says if the PendingIntent does not already exist,
        // return null instead of creating it
        PendingIntent pi = PendingIntent.getService(
            context, 0, intent, PendingIntent.FLAG_NO_CREATE);
        return pi != null;
    }

    private static BroadcastReceiver sNotificationReceiver =
        new BroadcastReceiver() {
        @Override
        public void onReceive(Context c, Intent intent) {
            Log.i(TAG, "received result: " + getResultCode());

            // Ensure a foreground activity hasn't cancelled the broadcast
            if (getResultCode() == Activity.RESULT_OK) {
                int requestCode = intent.getIntExtra("REQUEST_CODE", 0);
                Notification notification =
                    intent.getParcelableExtra("NOTIFICATION");

                NotificationManager notificationManager = (NotificationManager)
                        c.getSystemService(NOTIFICATION_SERVICE);

                // requestCode is the identifier for the notification and should
                // unique across the application.  You can implement a progress
                // bar or dynamic visual by passing in a second notification
                // with the same ID
                notificationManager.notify(requestCode, notification);
            }
        }
    };

    void showBackgroundNotification(int requestCode,
                                    Notification notification) {
        Intent intent = new Intent(ACTION_SHOW_NOTIFICATION);
        intent.putExtra("REQUEST_CODE", requestCode);
        intent.putExtra("NOTIFICATION", notification);

        // Regular broadcast intents are received by everyone at the same time.
        // Since onReceive is executed on the main thread, receivers are not
        // executed concurrently and are not executed in a particular oder.
        // Thus, it is difficult for broadcast receivers to communicate with
        // each other or for the sender of the intent to receive info from the
        // receivers
        // Ordered broadcast intents allow two-way communication by allowing a
        // sequence of broadcast receivers to process a broadcast intent in
        // order.  They also allow the sender of a broadcast to receive results
        // from the broadcast's recipients by passing in a special broadcast
        // receiver (result receiver).
        sendOrderedBroadcast(intent,
                             PERM_PRIVATE,
                             sNotificationReceiver,
                             null,
                             Activity.RESULT_OK,
                             null,
                             null);
    }
}


<|start_filename|>src/com/donnemartin/android/photogallery/FlickrConn.java<|end_filename|>
package com.donnemartin.android.photogallery;

import android.net.Uri;
import android.util.Log;
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.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownServiceException;
import java.util.ArrayList;

public class FlickrConn {

    public static final String TAG = "FlickerConn";

    public static final String PREF_SEARCH_QUERY ="searchQuery";
    public static final String PREF_LAST_RESULT_ID ="lastResultId";

    private static final String ENDPOINT =
        "https://api.flickr.com/services/rest/";
    private static final String METHOD_GET_RECENT = "flickr.photos.getRecent";
    private static final String METHOD_SEARCH = "flickr.photos.search";
    private static final String PARAM_EXTRAS = "extras";
    private static final String PARAM_TEXT = "text";

    // Specifies the name of the photo XML element used by XmlPullParser to
    // identify each photo in the XML
    private static final String XML_PHOTO = "photo";
    private static final String EXTRA_SMALL_URL = "url_s";

    public byte[] getUrlBytes(String urlSpec) throws IOException {
        URL url = new URL(urlSpec);
        // Cast to HttpURLConnection to give us HTTP-specific interfaces for
        // working with request methods, response codes, streaming methods, etc
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        byte[] outBytes = null;

        try {
            // Connect to the endpoint (GET), use getOutputStream for POST
            InputStream in = connection.getInputStream();

            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                int bytesRead;
                byte[] buffer = new byte[1024];

                // Call read repeatedly until the connection runs out of data
                // InputStream will yield bytes as they are available
                while ((bytesRead = in.read(buffer)) > 0) {
                    out.write(buffer, 0, bytesRead);
                }
                out.close();
                outBytes = out.toByteArray();
            }
        } catch (SocketTimeoutException ste) {
            Log.e(TAG, "Socket timeout trying to read bytes", ste);
        } catch (UnknownServiceException use) {
            Log.e(TAG, "Unknown service exception trying to read bytes", use);
        } catch (IOException ioe) {
            Log.e(TAG, "IO Exception trying read bytes", ioe);
        } finally {
            connection.disconnect();
        }

        return outBytes;
    }

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

    public ArrayList<GalleryItem> downloadGalleryItems(String  url) {
        ArrayList<GalleryItem> items = new ArrayList<GalleryItem>();

        try {
            String xmlString = getUrl(url);
            Log.i(TAG, "Received xml: " + xmlString);

            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            XmlPullParser parser = factory.newPullParser();
            parser.setInput(new StringReader(xmlString));

            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 ArrayList<GalleryItem> fetchItems() {
        // Build the complete URL for the API request with Uri.Builder,
        // a convenience class for creating properly escaped parameterized
        // URLs
        String url = Uri.parse(ENDPOINT).buildUpon()
            .appendQueryParameter("method", METHOD_GET_RECENT)
            .appendQueryParameter("api_key", Creds.API_KEY)
            .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL)
            .build().toString();
        return downloadGalleryItems(url);
    }

    public ArrayList<GalleryItem> search(String query) {
        String url = Uri.parse(ENDPOINT).buildUpon()
            .appendQueryParameter("method", METHOD_SEARCH)
            .appendQueryParameter("api_key", Creds.API_KEY)
            .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL)
            .appendQueryParameter(PARAM_TEXT, query)
            .build().toString();
        return downloadGalleryItems(url);
    }

    void parseItems(ArrayList<GalleryItem> items, XmlPullParser parser)
        throws XmlPullParserException, IOException {
        // XmlPullParser is used internally by Android to inflate layout files
        // It can also be used to parse GalleryItem objects
        //
        // Sample XML:
        // <photo id="14960257130" owner="76958130@N08" secret="204ab79ea2"
        // server="3861" farm="4" title="#tgif#s5" ispublic="1" isfriend="0"
        // isfamily="0" url_s="https://farm4.staticflickr.com/3861/14960257130_
        // 204ab79ea2_m.jpg" height_s="240" width_s="240" />
        int eventType = parser.next();

        while (eventType != XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.START_TAG &&
                XML_PHOTO.equals(parser.getName())) {
                String id = parser.getAttributeValue(null, "id");
                String caption = parser.getAttributeValue(null, "title");
                String smallUrl = parser.getAttributeValue(null,
                                                           EXTRA_SMALL_URL);
                String owner = parser.getAttributeValue(null, "owner");

                GalleryItem item = new GalleryItem();
                item.setId(id);
                item.setCaption(caption);
                item.setUrl(smallUrl);
                item.setOwner(owner);
                items.add(item);
            }

            eventType = parser.next();
        }
    }
}

<|start_filename|>src/com/donnemartin/android/photogallery/VisibleFragment.java<|end_filename|>
package com.donnemartin.android.photogallery;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.app.Fragment;
import android.util.Log;

public abstract class VisibleFragment extends Fragment {
    // Dynamic broadcast receiver.
    // Generic fragment that hides foreground notifications

    public static final String TAG = "VisibleFragment";

    // The receiver is typically defined as an inner instance like a
    // button click listener
    // We need mOnShowNotification for onResume and onPause, so save it
    private BroadcastReceiver mOnShowNotification = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // if we receive this, we're visible, so cancel
            // the notification
            Log.i(TAG, "canceling notification");
            setResultCode(Activity.RESULT_CANCELED);
        }
    };

    @Override
    public void onResume() {
        super.onResume();

        // Can also create this in XML as an <intent-filter>
        IntentFilter filter =
            new IntentFilter(PollService.ACTION_SHOW_NOTIFICATION);
        getActivity().registerReceiver(mOnShowNotification,
                                       filter,
                                       PollService.PERM_PRIVATE,
                                       null);
    }

    @Override
    public void onPause() {
        super.onPause();
        getActivity().unregisterReceiver(mOnShowNotification);
    }
}


<|start_filename|>src/com/donnemartin/android/photogallery/Creds.java<|end_filename|>
package com.donnemartin.android.photogallery;

public class Creds {

    public static final String API_KEY = "YOUR_FLICKR_API_KEY";
}


<|start_filename|>src/com/donnemartin/android/photogallery/ThumbnailDownloader.java<|end_filename|>
package com.donnemartin.android.photogallery;

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

import android.annotation.SuppressLint;
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 android.widget.ImageView;

public class ThumbnailDownloader extends HandlerThread {
    // Class to download thumbnails on demand to avoid isses when there is
    // a slow connection or a large number of thumbnails stored in memory
    // AsyncTask is the easiest way to get a background thread, but it is
    // not well suited for repetitive and long-running work.

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

    Handler mHandler;
    // Synchronized hash map.  Using Token as a key, store and retrieve the
    // URL associated with a particular Token.
    Map<ImageView,String> requestMap =
            Collections.synchronizedMap(new HashMap<ImageView,String>());
    Handler mResponseHandler;
    Listener<ImageView> mListener;

    public interface Listener<ImageView> {
        // Add a Listener interface to communicate the responses with
        void onThumbnailDownloaded(ImageView imageView, Bitmap thumbnail);
    }

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

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

    // Suppress warnings about subclassing Handler.
    // The handler will be kept alive by its Looper.  If your handler is an
    // anonymous inner class, it is easy to leak memory accidentally through an
    // implicit object reference.  However, everything here is tied to the
    // HandlerThread so there is no danger of leaking anything
    @SuppressLint("HandlerLeak")
    @Override
    protected void onLooperPrepared() {
        // onLooperPrepared is called before the Looper checks the queue
        // for the first time, a good place to create a Handler implementation
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == MESSAGE_DOWNLOAD) {
                    // Necessary because Token is a generic class argument but
                    // Message.obj is an Object.  Type erasure makes it
                    // impossible to make this cast
                    @SuppressWarnings("unchecked")
                    ImageView imageView = (ImageView)msg.obj;
                    Log.i(TAG,
                      "Got a request for url: " + requestMap.get(imageView));
                    handleRequest(imageView);
                }
            }
        };
    }

    private void handleRequest(final ImageView imageView) {
        // Use FlickrConn to download bytes from the URL and turn them
        // into a bitmap
        try {
            final String url = requestMap.get(imageView);
            if (url != null) {
                byte[] bitmapBytes = new FlickrConn().getUrlBytes(url);

                // Create a BitmapFactory to construct a bitmap with the array
                // of bytes
                final Bitmap bitmap = BitmapFactory
                        .decodeByteArray(bitmapBytes, 0, bitmapBytes.length);

                // Send a custom message back to the main thread
                // When a Message has its callback field set, instead of being
                // rn by its Handler target, the Runnable in callback is run
                // instead.  Because mResponseHandler is associated with the
                // main thread's Looper, this UI update code will be run on the
                // main thread
                mResponseHandler.post(new Runnable() {
                    public void run() {
                        // Double check the requestMap because the GridView
                        // recycles its views to ensure the Token gets the
                        // correct image, even if another request has been
                        // made in the meantime
                        String urlStored = requestMap.get(imageView);
                        if (urlStored != null && urlStored.equals(url)) {
                            // Remove this token since we have handled it
                            requestMap.remove(imageView);
                            // Set the bitmap on the token
                            mListener.onThumbnailDownloaded(imageView, bitmap);
                        }
                    }
                });
            }
        } catch (IOException ioe) {
            Log.e(TAG, "Error downloading image", ioe);
        }
    }

    public void queueThumbnail(ImageView imageView, String url) {
        requestMap.put(imageView, url);

        // Build the message using obtainMessage rather than building it by hand
        // ObtainMessage pulls from a common recycling pool to avoid creating
        // new message objects
        mHandler
            .obtainMessage(MESSAGE_DOWNLOAD, imageView)
            .sendToTarget();
    }

    public void clearQueue() {
        // If the user rotates the screen, ThumbnailDownloader might have an
        // invalid ImageView.  Clean all requests out of the queue.
        mHandler.removeMessages(MESSAGE_DOWNLOAD);
        requestMap.clear();
    }
}


<|start_filename|>src/com/donnemartin/android/photogallery/PhotoPageActivity.java<|end_filename|>
package com.donnemartin.android.photogallery;

import android.support.v4.app.Fragment;

public class PhotoPageActivity extends SingleFragmentActivity {

    @Override
    public Fragment createFragment() {
        return new PhotoPageFragment();
    }
}


<|start_filename|>src/com/donnemartin/android/photogallery/PhotoGalleryFragment.java<|end_filename|>
package com.donnemartin.android.photogallery;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.SearchManager;
import android.app.SearchableInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.view.*;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.SearchView;

import java.util.ArrayList;

public class PhotoGalleryFragment extends VisibleFragment {

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

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

        setRetainInstance(true);
        setHasOptionsMenu(true);

        updateItems();

        // By default, handler will attach itself to the Looper of the current
        // thread, which is the main thread
        mThumbnailThread = new ThumbnailDownloader(new Handler());
        mThumbnailThread
            .setListener(new ThumbnailDownloader.Listener<ImageView>() {
            public void onThumbnailDownloaded(ImageView imageView,
                                              Bitmap thumbnail) {
                // isVisible ensures we are not setting the image on a stale
                // ImageView
                if (isVisible()) {
                    imageView.setImageBitmap(thumbnail);
                }
            }
        });
        mThumbnailThread.start();

        // Ensure the thread's guts are ready before proceeding
        mThumbnailThread.getLooper();
    }

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

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

        // Ensure we configure the appropriate adapter when the new GridView
        // is created, such as on a rotation.
        setupAdapter();

        mGridView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> gridView,
                                    View view,
                                    int pos,
                                    long id) {
                GalleryItem item = mItems.get(pos);

                Uri photoPageUri = Uri.parse(item.getPhotoPageUrl());
                Intent intent = new Intent(getActivity(),
                                           PhotoPageActivity.class);
                intent.setData(photoPageUri);

                startActivity(intent);
            }
        });

        return view;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // It's critical to quit the HandlerThread otherwise it will never die
        mThumbnailThread.quit();
    }

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

    @Override
    @TargetApi(11)
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.fragment_photo_gallery, menu);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // Pull out the SearchView
            MenuItem searchItem = menu.findItem(R.id.menu_item_search);
            SearchView searchView = (SearchView)searchItem.getActionView();

            // Get the data from our searchable.xml as a SearchableInfo
            // SearchManager is a system service that is responsible for all
            // things search related
            SearchManager searchManager = (SearchManager)getActivity()
                .getSystemService(Context.SEARCH_SERVICE);
            ComponentName name = getActivity().getComponentName();
            // Root around in the manifest, package up the relevant info
            SearchableInfo searchInfo = searchManager.getSearchableInfo(name);

            searchView.setSearchableInfo(searchInfo);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Note, SearchView does not generate any events here
        // These callbacks are only for older devices
        boolean selectionHandled;

        switch (item.getItemId()) {
            case R.id.menu_item_search:
                getActivity().onSearchRequested();
                selectionHandled = true;
                break;
            case R.id.menu_item_clear:
                PreferenceManager.getDefaultSharedPreferences(getActivity())
                        .edit()
                        .putString(FlickrConn.PREF_SEARCH_QUERY, null)
                        .commit();
                updateItems();
                selectionHandled = true;
                break;
            case R.id.menu_item_toggle_polling:
                boolean shouldStartAlarm =
                    !PollService.isServiceAlarmOn(getActivity());
                PollService.setServiceAlarm(getActivity(), shouldStartAlarm);

                // On 3.0 and later devices, the action bar does not
                // automatically update itself
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                    getActivity().invalidateOptionsMenu();
                }

                selectionHandled = true;
                break;
            default:
                selectionHandled = super.onOptionsItemSelected(item);
        }

        return selectionHandled;
    }

    @Override
    // Called whenever the menu needs to be configured
    public void onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);

        MenuItem toggleItem = menu.findItem(R.id.menu_item_toggle_polling);
        if (PollService.isServiceAlarmOn(getActivity())) {
            toggleItem.setTitle(R.string.stop_polling);
        } else {
            toggleItem.setTitle(R.string.start_polling);
        }
    }

    public void updateItems() {
        // Start AsyncTask to fire up a background thread and calls
        // doInBackground() method
        new FetchItemsTask().execute();
    }

    void setupAdapter() {
        // GridView has no GridFragment class, so we have to build our own
        // adapter management.  We look at the current model state and configure
        // the adapter appropriately

        // Fragments can exist unattached from any activity, which can occur
        // since we are using AsyncTask.
        if (getActivity() != null && mGridView != null) {
            if (mItems != null) {
                mGridView.setAdapter(new GalleryItemAdapter(mItems));
            } else {
                mGridView.setAdapter(null);
            }
        }
    }

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

        @Override
        protected ArrayList<GalleryItem> doInBackground(Void... params) {
            ArrayList<GalleryItem> list;
            Activity activity = getActivity();

            if (activity == null) {
                list = new ArrayList<GalleryItem>();
            } else {
                String query = PreferenceManager
                    .getDefaultSharedPreferences(activity)
                    .getString(FlickrConn.PREF_SEARCH_QUERY, null);

                if (query != null) {
                    list = new FlickrConn().search(query);
                }
                else {
                    list = new FlickrConn().fetchItems();
                }
            }

            return list;
        }

        @Override
        protected void onPostExecute(ArrayList<GalleryItem> items) {
            // onPostExecute is run after doInBackground completes and is run
            // on the main thread, not the background thread so it is safe
            // to update the UI within it
            mItems = items;

            if (items.size() > 0) {
                String resultId = items.get(0).getId();
                PreferenceManager.getDefaultSharedPreferences(getActivity())
                    .edit()
                    .putString(FlickrConn.PREF_LAST_RESULT_ID, resultId)
                    .commit();
            }

            // Configure the appropriate adapter when we update our items
            setupAdapter();
        }
    }

    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);
            }

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

            return convertView;
        }
    }
}
