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