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 
17 package com.example.android.threadsample;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.database.Cursor;
22 import android.graphics.drawable.Drawable;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.support.v4.app.Fragment;
26 import android.support.v4.app.FragmentTransaction;
27 import android.support.v4.app.LoaderManager;
28 import android.support.v4.content.CursorLoader;
29 import android.support.v4.content.Loader;
30 import android.support.v4.content.LocalBroadcastManager;
31 import android.support.v4.widget.CursorAdapter;
32 import android.util.DisplayMetrics;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.ViewGroup.LayoutParams;
37 import android.widget.AbsListView;
38 import android.widget.AdapterView;
39 import android.widget.GridView;
40 
41 import java.net.MalformedURLException;
42 import java.net.URL;
43 import java.util.concurrent.RejectedExecutionException;
44 
45 /**
46  * PhotoThumbnailFragment displays a GridView of picture thumbnails downloaded from Picasa
47  */
48 public 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
onCreateLoader(int loaderID, Bundle bundle)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
onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle)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
onDestroyView()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
onDetach()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
onHiddenChanged(boolean viewState)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
onItemClick(AdapterView<?> adapterView, View view, int viewId, long rowId)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
onLoadFinished(Loader<Cursor> loader, Cursor returnCursor)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
onLoaderReset(Loader<Cursor> loader)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
onSaveInstanceState(Bundle bundle)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
setLoaded(boolean loadState)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          */
GridViewAdapter(Context context)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
bindView(View view, Context context, Cursor cursor)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
newView(Context context, Cursor cursor, ViewGroup viewGroup)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