1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.example.android.threadsample;
18
19import android.content.Context;
20import android.content.Intent;
21import android.database.Cursor;
22import android.graphics.drawable.Drawable;
23import android.net.Uri;
24import android.os.Bundle;
25import android.support.v4.app.Fragment;
26import android.support.v4.app.FragmentTransaction;
27import android.support.v4.app.LoaderManager;
28import android.support.v4.content.CursorLoader;
29import android.support.v4.content.Loader;
30import android.support.v4.content.LocalBroadcastManager;
31import android.support.v4.widget.CursorAdapter;
32import android.util.DisplayMetrics;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36import android.view.ViewGroup.LayoutParams;
37import android.widget.AbsListView;
38import android.widget.AdapterView;
39import android.widget.GridView;
40
41import java.net.MalformedURLException;
42import java.net.URL;
43import java.util.concurrent.RejectedExecutionException;
44
45/**
46 * PhotoThumbnailFragment displays a GridView of picture thumbnails downloaded from Picasa
47 */
48public class PhotoThumbnailFragment extends Fragment implements
49        LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener {
50
51    private static final String STATE_IS_HIDDEN =
52            "com.example.android.threadsample.STATE_IS_HIDDEN";
53
54    // The width of each column in the grid
55    private int mColumnWidth;
56
57    // A Drawable for a grid cell that's empty
58    private Drawable mEmptyDrawable;
59
60    // The GridView for displaying thumbnails
61    private GridView mGridView;
62
63    // Denotes if the GridView has been loaded
64    private boolean mIsLoaded;
65
66    // Intent for starting the IntentService that downloads the Picasa featured picture RSS feed
67    private Intent mServiceIntent;
68
69    // An adapter between a Cursor and the Fragment's GridView
70    private GridViewAdapter mAdapter;
71
72    // The URL of the Picasa featured picture RSS feed, in String format
73    private static final String PICASA_RSS_URL =
74            "http://picasaweb.google.com/data/feed/base/featured?" +
75            "alt=rss&kind=photo&access=public&slabel=featured&hl=en_US&imgmax=1600";
76
77    private static final String[] PROJECTION =
78    {
79        DataProviderContract._ID,
80        DataProviderContract.IMAGE_THUMBURL_COLUMN,
81        DataProviderContract.IMAGE_URL_COLUMN
82    };
83
84    // Constants that define the order of columns in the returned cursor
85    private static final int IMAGE_THUMBURL_CURSOR_INDEX = 1;
86    private static final int IMAGE_URL_CURSOR_INDEX = 2;
87
88    // Identifies a particular Loader being used in this component
89    private static final int URL_LOADER = 0;
90
91    /*
92     * This callback is invoked when the framework is starting or re-starting the Loader. It
93     * returns a CursorLoader object containing the desired query
94     */
95    @Override
96    public Loader<Cursor> onCreateLoader(int loaderID, Bundle bundle)
97    {
98        /*
99         * Takes action based on the ID of the Loader that's being created
100         */
101        switch (loaderID) {
102            case URL_LOADER:
103            // Returns a new CursorLoader
104            return new CursorLoader(
105                        getActivity(),                                     // Context
106                        DataProviderContract.PICTUREURL_TABLE_CONTENTURI,  // Table to query
107                        PROJECTION,                                        // Projection to return
108                        null,                                              // No selection clause
109                        null,                                              // No selection arguments
110                        null                                               // Default sort order
111            );
112            default:
113                // An invalid id was passed in
114                return null;
115
116        }
117
118    }
119
120    /*
121     * This callback is invoked when the the Fragment's View is being loaded. It sets up the View.
122     */
123    @Override
124    public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
125
126        // Always call the super method first
127        super.onCreateView(inflater, viewGroup, bundle);
128
129        /*
130         * Inflates the View from the gridlist layout file, using the layout parameters in
131         * "viewGroup"
132         */
133        View localView = inflater.inflate(R.layout.gridlist, viewGroup, false);
134
135        // Sets the View's data adapter to be a new GridViewAdapter
136        mAdapter = new GridViewAdapter(getActivity());
137
138        // Gets a handle to the GridView in the layout
139        mGridView = ((GridView) localView.findViewById(android.R.id.list));
140
141        // Instantiates a DisplayMetrics object
142        DisplayMetrics localDisplayMetrics = new DisplayMetrics();
143
144        // Gets the current display metrics from the current Window
145        getActivity().getWindowManager().getDefaultDisplay().getMetrics(localDisplayMetrics);
146
147        /*
148         * Gets the dp value from the thumbSize resource as an integer in dps. The value can
149         * be adjusted for specific display sizes, etc. in the dimens.xml file for a particular
150         * values-<qualifier> directory
151         */
152        int pixelSize = getResources().getDimensionPixelSize(R.dimen.thumbSize);
153
154        /*
155         * Calculates a width scale factor from the pixel width of the current display and the
156         * desired pixel size
157         */
158        int widthScale = localDisplayMetrics.widthPixels / pixelSize;
159
160        // Calculates the grid column width
161        mColumnWidth = (localDisplayMetrics.widthPixels / widthScale);
162
163        // Sets the GridView's column width
164        mGridView.setColumnWidth(mColumnWidth);
165
166        // Starts by setting the GridView to have no columns
167        mGridView.setNumColumns(-1);
168
169        // Sets the GridView's data adapter
170        mGridView.setAdapter(mAdapter);
171
172        /*
173         * Sets the GridView's click listener to be this class. As a result, when users click the
174         * GridView, PhotoThumbnailFragment.onClick() is invoked.
175         */
176        mGridView.setOnItemClickListener(this);
177
178        /*
179         * Sets the "empty" View for the layout. If there's nothing to show, a ProgressBar
180         * is displayed.
181         */
182        mGridView.setEmptyView(localView.findViewById(R.id.progressRoot));
183
184        // Sets a dark background to show when no image is queued to be downloaded
185        mEmptyDrawable = getResources().getDrawable(R.drawable.imagenotqueued);
186
187        // Initializes the CursorLoader
188        getLoaderManager().initLoader(URL_LOADER, null, this);
189
190        /*
191         * Creates a new Intent to send to the download IntentService. The Intent contains the
192         * URL of the Picasa feature picture RSS feed
193         */
194        mServiceIntent =
195                new Intent(getActivity(), RSSPullService.class)
196                        .setData(Uri.parse(PICASA_RSS_URL));
197
198        // If there's no pre-existing state for this Fragment
199        if (bundle == null) {
200            // If the data wasn't previously loaded
201            if (!this.mIsLoaded) {
202                // Starts the IntentService to download the RSS feed data
203                getActivity().startService(mServiceIntent);
204            }
205
206        // If this Fragment existed previously, gets its state
207        } else if (bundle.getBoolean(STATE_IS_HIDDEN, false)) {
208
209            // Begins a transaction
210            FragmentTransaction localFragmentTransaction =
211                    getFragmentManager().beginTransaction();
212
213            // Hides the Fragment
214            localFragmentTransaction.hide(this);
215
216            // Commits the transaction
217            localFragmentTransaction.commit();
218        }
219
220        // Returns the View inflated from the layout
221        return localView;
222    }
223
224    /*
225     * This callback is invoked when the Fragment is being destroyed.
226     */
227    @Override
228    public void onDestroyView() {
229
230        // Sets variables to null, to avoid memory leaks
231        mGridView = null;
232
233        // If the EmptyDrawable contains something, sets those members to null
234        if (mEmptyDrawable != null) {
235            this.mEmptyDrawable.setCallback(null);
236            this.mEmptyDrawable = null;
237        }
238
239        // Always call the super method last
240        super.onDestroyView();
241    }
242
243    /*
244     * This callback is invoked after onDestroyView(). It clears out variables, shuts down the
245     * CursorLoader, and so forth
246     */
247    @Override
248    public void onDetach() {
249
250        // Destroys variables and references, and catches Exceptions
251        try {
252            getLoaderManager().destroyLoader(0);
253            if (mAdapter != null) {
254                mAdapter.changeCursor(null);
255                mAdapter = null;
256            }
257        } catch (Throwable localThrowable) {
258        }
259
260        // Always call the super method last
261        super.onDetach();
262        return;
263    }
264
265    /*
266     * This is invoked whenever the visibility state of the Fragment changes
267     */
268    @Override
269    public void onHiddenChanged(boolean viewState) {
270        super.onHiddenChanged(viewState);
271    }
272
273    /*
274     * Implements OnItemClickListener.onItemClick() for this View's listener.
275     * This implementation detects the View that was clicked and retrieves its picture URL.
276     */
277    @Override
278    public void onItemClick(AdapterView<?> adapterView, View view, int viewId, long rowId) {
279
280        // Returns a one-row cursor for the data that backs the View that was clicked.
281        Cursor cursor = (Cursor) mAdapter.getItem(viewId);
282
283        // Retrieves the urlString from the cursor
284        String urlString = cursor.getString(IMAGE_URL_CURSOR_INDEX);
285
286        /*
287         * Creates a new Intent to get the full picture for the thumbnail that the user clicked.
288         * The full photo is loaded into a separate Fragment
289         */
290        Intent localIntent =
291                new Intent(Constants.ACTION_VIEW_IMAGE)
292                .setData(Uri.parse(urlString));
293
294        // Broadcasts the Intent to receivers in this app. See DisplayActivity.FragmentDisplayer.
295        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(localIntent);
296    }
297
298    /*
299     * Invoked when the CursorLoader finishes the query. A reference to the Loader and the
300     * returned Cursor are passed in as arguments
301     */
302    @Override
303    public void onLoadFinished(Loader<Cursor> loader, Cursor returnCursor) {
304
305        /*
306         *  Changes the adapter's Cursor to be the results of the load. This forces the View to
307         *  redraw.
308         */
309
310        mAdapter.changeCursor(returnCursor);
311    }
312
313    /*
314     * Invoked when the CursorLoader is being reset. For example, this is called if the
315     * data in the provider changes and the Cursor becomes stale.
316     */
317    @Override
318    public void onLoaderReset(Loader<Cursor> loader) {
319
320        // Sets the Adapter's backing data to null. This prevents memory leaks.
321        mAdapter.changeCursor(null);
322    }
323
324    /*
325     * This callback is invoked when the system has to destroy the Fragment for some reason. It
326     * allows the Fragment to save its state, so the state can be restored later on.
327     */
328    @Override
329    public void onSaveInstanceState(Bundle bundle) {
330
331        // Saves the show-hide status of the display
332        bundle.putBoolean(STATE_IS_HIDDEN, isHidden());
333
334        // Always call the super method last
335        super.onSaveInstanceState(bundle);
336    }
337
338    // Sets the state of the loaded flag
339    public void setLoaded(boolean loadState) {
340        mIsLoaded = loadState;
341    }
342
343    /**
344     * Defines a custom View adapter that extends CursorAdapter. The main reason to do this is to
345     * display images based on the backing Cursor, rather than just displaying the URLs that the
346     * Cursor contains.
347     */
348    private class GridViewAdapter extends CursorAdapter {
349
350        /**
351         * Simplified constructor that calls the super constructor with the input Context,
352         * a null value for Cursor, and no flags
353         * @param context A Context for this object
354         */
355        public GridViewAdapter(Context context) {
356            super(context, null, false);
357        }
358
359        /**
360         *
361         * Binds a View and a Cursor
362         *
363         * @param view An existing View object
364         * @param context A Context for the View and Cursor
365         * @param cursor The Cursor to bind to the View, representing one row of the returned query.
366         */
367        @Override
368        public void bindView(View view, Context context, Cursor cursor) {
369
370            // Gets a handle to the View
371            PhotoView localImageDownloaderView = (PhotoView) view.getTag();
372
373            // Converts the URL string to a URL and tries to retrieve the picture
374            try {
375                // Gets the URL
376                URL localURL =
377                        new URL(
378                            cursor.getString(IMAGE_THUMBURL_CURSOR_INDEX)
379                        )
380                ;
381                /*
382                 * Invokes setImageURL for the View. If the image isn't already available, this
383                 * will download and decode it.
384                 */
385                localImageDownloaderView.setImageURL(
386                            localURL, true, PhotoThumbnailFragment.this.mEmptyDrawable);
387
388            // Catches an invalid URL
389            } catch (MalformedURLException localMalformedURLException) {
390                localMalformedURLException.printStackTrace();
391
392            // Catches errors trying to download and decode the picture in a ThreadPool
393            } catch (RejectedExecutionException localRejectedExecutionException) {
394            }
395        }
396
397        /**
398         * Creates a new View that shows the contents of the Cursor
399         *
400         *
401         * @param context A Context for the View and Cursor
402         * @param cursor The Cursor to display. This is a single row of the returned query
403         * @param viewGroup The viewGroup that's the parent of the new View
404         * @return the newly-created View
405         */
406        @Override
407        public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
408            // Gets a new layout inflater instance
409            LayoutInflater inflater = LayoutInflater.from(context);
410
411            /*
412             * Creates a new View by inflating the specified layout file. The root ViewGroup is
413             * the root of the layout file. This View is a FrameLayout
414             */
415            View layoutView = inflater.inflate(R.layout.galleryitem, null);
416
417            /*
418             * Creates a second View to hold the thumbnail image.
419             */
420            View thumbView = layoutView.findViewById(R.id.thumbImage);
421
422            /*
423             * Sets layout parameters for the layout based on the layout parameters of a virtual
424             * list. In addition, this sets the layoutView's width to be MATCH_PARENT, and its
425             * height to be the column width?
426             */
427            layoutView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
428                    PhotoThumbnailFragment.this.mColumnWidth));
429
430            // Sets the layoutView's tag to be the same as the thumbnail image tag.
431            layoutView.setTag(thumbView);
432            return layoutView;
433        }
434
435    }
436}
437