1 /* 2 * Copyright (C) 2011 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.ex.photo.fragments; 19 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.database.Cursor; 25 import android.graphics.Bitmap; 26 import android.net.ConnectivityManager; 27 import android.net.NetworkInfo; 28 import android.os.Bundle; 29 import android.support.v4.app.Fragment; 30 import android.support.v4.app.LoaderManager; 31 import android.support.v4.content.Loader; 32 import android.util.DisplayMetrics; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.View.OnClickListener; 36 import android.view.ViewGroup; 37 import android.view.WindowManager; 38 import android.widget.ImageView; 39 import android.widget.ProgressBar; 40 import android.widget.TextView; 41 42 43 import com.android.ex.photo.Intents; 44 import com.android.ex.photo.PhotoViewCallbacks; 45 import com.android.ex.photo.PhotoViewCallbacks.CursorChangedListener; 46 import com.android.ex.photo.PhotoViewCallbacks.OnScreenListener; 47 import com.android.ex.photo.R; 48 import com.android.ex.photo.adapters.PhotoPagerAdapter; 49 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; 50 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface; 51 import com.android.ex.photo.util.ImageUtils; 52 import com.android.ex.photo.views.PhotoView; 53 import com.android.ex.photo.views.ProgressBarWrapper; 54 55 /** 56 * Displays a photo. 57 */ 58 public class PhotoViewFragment extends Fragment implements 59 LoaderManager.LoaderCallbacks<BitmapResult>, 60 OnClickListener, 61 OnScreenListener, 62 CursorChangedListener { 63 64 /** 65 * Interface for components that are internally scrollable left-to-right. 66 */ 67 public static interface HorizontallyScrollable { 68 /** 69 * Return {@code true} if the component needs to receive right-to-left 70 * touch movements. 71 * 72 * @param origX the raw x coordinate of the initial touch 73 * @param origY the raw y coordinate of the initial touch 74 */ 75 76 public boolean interceptMoveLeft(float origX, float origY); 77 78 /** 79 * Return {@code true} if the component needs to receive left-to-right 80 * touch movements. 81 * 82 * @param origX the raw x coordinate of the initial touch 83 * @param origY the raw y coordinate of the initial touch 84 */ 85 public boolean interceptMoveRight(float origX, float origY); 86 } 87 88 protected final static String STATE_INTENT_KEY = 89 "com.android.mail.photo.fragments.PhotoViewFragment.INTENT"; 90 91 protected final static String ARG_INTENT = "arg-intent"; 92 protected final static String ARG_POSITION = "arg-position"; 93 protected final static String ARG_SHOW_SPINNER = "arg-show-spinner"; 94 95 /** The size of the photo */ 96 public static Integer sPhotoSize; 97 98 /** The URL of a photo to display */ 99 protected String mResolvedPhotoUri; 100 protected String mThumbnailUri; 101 /** The intent we were launched with */ 102 protected Intent mIntent; 103 protected PhotoViewCallbacks mCallback; 104 protected PhotoPagerAdapter mAdapter; 105 106 protected BroadcastReceiver mInternetStateReceiver; 107 108 protected PhotoView mPhotoView; 109 protected ImageView mPhotoPreviewImage; 110 protected TextView mEmptyText; 111 protected ImageView mRetryButton; 112 protected ProgressBarWrapper mPhotoProgressBar; 113 114 protected int mPosition; 115 116 /** Whether or not the fragment should make the photo full-screen */ 117 protected boolean mFullScreen; 118 119 /** 120 * True if the PhotoViewFragment should watch the network state in order to restart loaders. 121 */ 122 protected boolean mWatchNetworkState; 123 124 /** Whether or not this fragment will only show the loading spinner */ 125 protected boolean mOnlyShowSpinner; 126 127 /** Whether or not the progress bar is showing valid information about the progress stated */ 128 protected boolean mProgressBarNeeded = true; 129 130 protected View mPhotoPreviewAndProgress; 131 protected boolean mThumbnailShown; 132 133 /** Whether or not there is currently a connection to the internet */ 134 protected boolean mConnected; 135 136 /** Whether or not we can display the thumbnail at fullscreen size */ 137 protected boolean mDisplayThumbsFullScreen; 138 139 /** Public no-arg constructor for allowing the framework to handle orientation changes */ 140 public PhotoViewFragment() { 141 // Do nothing. 142 } 143 144 /** 145 * Create a {@link PhotoViewFragment}. 146 * @param intent 147 * @param position 148 * @param onlyShowSpinner 149 */ 150 public static PhotoViewFragment newInstance( 151 Intent intent, int position, boolean onlyShowSpinner) { 152 final Bundle b = new Bundle(); 153 b.putParcelable(ARG_INTENT, intent); 154 b.putInt(ARG_POSITION, position); 155 b.putBoolean(ARG_SHOW_SPINNER, onlyShowSpinner); 156 final PhotoViewFragment f = new PhotoViewFragment(); 157 f.setArguments(b); 158 return f; 159 } 160 161 @Override 162 public void onActivityCreated(Bundle savedInstanceState) { 163 super.onActivityCreated(savedInstanceState); 164 mCallback = (PhotoViewCallbacks) getActivity(); 165 if (mCallback == null) { 166 throw new IllegalArgumentException( 167 "Activity must be a derived class of PhotoViewActivity"); 168 } 169 mAdapter = mCallback.getAdapter(); 170 if (mAdapter == null) { 171 throw new IllegalStateException("Callback reported null adapter"); 172 } 173 // Don't call until we've setup the entire view 174 setViewVisibility(); 175 } 176 177 @Override 178 public void onDetach() { 179 mCallback = null; 180 super.onDetach(); 181 } 182 183 @Override 184 public void onCreate(Bundle savedInstanceState) { 185 super.onCreate(savedInstanceState); 186 if (sPhotoSize == null) { 187 final DisplayMetrics metrics = new DisplayMetrics(); 188 final WindowManager wm = 189 (WindowManager) getActivity().getSystemService(Context.WINDOW_SERVICE); 190 final ImageUtils.ImageSize imageSize = ImageUtils.sUseImageSize; 191 wm.getDefaultDisplay().getMetrics(metrics); 192 switch (imageSize) { 193 case EXTRA_SMALL: 194 // Use a photo that's 80% of the "small" size 195 sPhotoSize = (Math.min(metrics.heightPixels, metrics.widthPixels) * 800) / 1000; 196 break; 197 case SMALL: 198 // Fall through. 199 case NORMAL: 200 // Fall through. 201 default: 202 sPhotoSize = Math.min(metrics.heightPixels, metrics.widthPixels); 203 break; 204 } 205 } 206 207 final Bundle bundle = getArguments(); 208 if (bundle == null) { 209 return; 210 } 211 mIntent = bundle.getParcelable(ARG_INTENT); 212 mDisplayThumbsFullScreen = mIntent.getBooleanExtra( 213 Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false); 214 215 mPosition = bundle.getInt(ARG_POSITION); 216 mOnlyShowSpinner = bundle.getBoolean(ARG_SHOW_SPINNER); 217 mProgressBarNeeded = true; 218 219 if (savedInstanceState != null) { 220 final Bundle state = savedInstanceState.getBundle(STATE_INTENT_KEY); 221 if (state != null) { 222 mIntent = new Intent().putExtras(state); 223 } 224 } 225 226 if (mIntent != null) { 227 mResolvedPhotoUri = mIntent.getStringExtra(Intents.EXTRA_RESOLVED_PHOTO_URI); 228 mThumbnailUri = mIntent.getStringExtra(Intents.EXTRA_THUMBNAIL_URI); 229 mWatchNetworkState = mIntent.getBooleanExtra(Intents.EXTRA_WATCH_NETWORK, false); 230 } 231 } 232 233 @Override 234 public View onCreateView(LayoutInflater inflater, ViewGroup container, 235 Bundle savedInstanceState) { 236 final View view = inflater.inflate(R.layout.photo_fragment_view, container, false); 237 initializeView(view); 238 return view; 239 } 240 241 protected void initializeView(View view) { 242 mPhotoView = (PhotoView) view.findViewById(R.id.photo_view); 243 mPhotoView.setMaxInitialScale(mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1)); 244 mPhotoView.setOnClickListener(this); 245 mPhotoView.setFullScreen(mFullScreen, false); 246 mPhotoView.enableImageTransforms(false); 247 248 mPhotoPreviewAndProgress = view.findViewById(R.id.photo_preview); 249 mPhotoPreviewImage = (ImageView) view.findViewById(R.id.photo_preview_image); 250 mThumbnailShown = false; 251 final ProgressBar indeterminate = 252 (ProgressBar) view.findViewById(R.id.indeterminate_progress); 253 final ProgressBar determinate = 254 (ProgressBar) view.findViewById(R.id.determinate_progress); 255 mPhotoProgressBar = new ProgressBarWrapper(determinate, indeterminate, true); 256 mEmptyText = (TextView) view.findViewById(R.id.empty_text); 257 mRetryButton = (ImageView) view.findViewById(R.id.retry_button); 258 259 // Don't call until we've setup the entire view 260 setViewVisibility(); 261 } 262 263 @Override 264 public void onResume() { 265 super.onResume(); 266 mCallback.addScreenListener(mPosition, this); 267 mCallback.addCursorListener(this); 268 269 if (mWatchNetworkState) { 270 if (mInternetStateReceiver == null) { 271 mInternetStateReceiver = new InternetStateBroadcastReceiver(); 272 } 273 getActivity().registerReceiver(mInternetStateReceiver, 274 new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); 275 ConnectivityManager connectivityManager = (ConnectivityManager) 276 getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); 277 NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); 278 if (activeNetInfo != null) { 279 mConnected = activeNetInfo.isConnected(); 280 } else { 281 // Best to set this to false, since it won't stop us from trying to download, 282 // only allow us to try re-download if we get notified that we do have a connection. 283 mConnected = false; 284 } 285 } 286 287 if (!isPhotoBound()) { 288 mProgressBarNeeded = true; 289 mPhotoPreviewAndProgress.setVisibility(View.VISIBLE); 290 291 getLoaderManager().initLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, 292 null, this); 293 294 // FLAG: If we are displaying thumbnails at fullscreen size, then we 295 // could defer the loading of the fullscreen image until the thumbnail 296 // has finished loading, or even until the user attempts to zoom in. 297 getLoaderManager().initLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO, 298 null, this); 299 } 300 } 301 302 @Override 303 public void onPause() { 304 // Remove listeners 305 if (mWatchNetworkState) { 306 getActivity().unregisterReceiver(mInternetStateReceiver); 307 } 308 mCallback.removeCursorListener(this); 309 mCallback.removeScreenListener(mPosition); 310 resetPhotoView(); 311 super.onPause(); 312 } 313 314 @Override 315 public void onDestroyView() { 316 // Clean up views and other components 317 if (mPhotoView != null) { 318 mPhotoView.clear(); 319 mPhotoView = null; 320 } 321 super.onDestroyView(); 322 } 323 324 public String getPhotoUri() { 325 return mResolvedPhotoUri; 326 } 327 328 @Override 329 public void onSaveInstanceState(Bundle outState) { 330 super.onSaveInstanceState(outState); 331 332 if (mIntent != null) { 333 outState.putParcelable(STATE_INTENT_KEY, mIntent.getExtras()); 334 } 335 } 336 337 @Override 338 public Loader<BitmapResult> onCreateLoader(int id, Bundle args) { 339 if(mOnlyShowSpinner) { 340 return null; 341 } 342 String uri = null; 343 switch (id) { 344 case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: 345 uri = mThumbnailUri; 346 break; 347 case PhotoViewCallbacks.BITMAP_LOADER_PHOTO: 348 uri = mResolvedPhotoUri; 349 break; 350 } 351 return mCallback.onCreateBitmapLoader(id, args, uri); 352 } 353 354 @Override 355 public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) { 356 Bitmap data = result.bitmap; 357 // If we don't have a view, the fragment has been paused. We'll get the cursor again later. 358 if (getView() == null) { 359 return; 360 } 361 362 final int id = loader.getId(); 363 switch (id) { 364 case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: 365 if (mDisplayThumbsFullScreen) { 366 displayPhoto(result); 367 } else { 368 if (isPhotoBound()) { 369 // There is need to do anything with the thumbnail 370 // image, as the full size image is being shown. 371 return; 372 } 373 374 if (data == null) { 375 // no preview, show default 376 mPhotoPreviewImage.setImageResource(R.drawable.default_image); 377 mThumbnailShown = false; 378 } else { 379 // show preview 380 mPhotoPreviewImage.setImageBitmap(data); 381 mThumbnailShown = true; 382 } 383 mPhotoPreviewImage.setVisibility(View.VISIBLE); 384 if (getResources().getBoolean(R.bool.force_thumbnail_no_scaling)) { 385 mPhotoPreviewImage.setScaleType(ImageView.ScaleType.CENTER); 386 } 387 enableImageTransforms(false); 388 } 389 break; 390 391 case PhotoViewCallbacks.BITMAP_LOADER_PHOTO: 392 displayPhoto(result); 393 break; 394 default: 395 break; 396 } 397 398 if (mProgressBarNeeded == false) { 399 // Hide the progress bar as it isn't needed anymore. 400 mPhotoProgressBar.setVisibility(View.GONE); 401 } 402 403 if (data != null) { 404 mCallback.onNewPhotoLoaded(mPosition); 405 } 406 setViewVisibility(); 407 } 408 409 private void displayPhoto(BitmapResult result) { 410 Bitmap data = result.bitmap; 411 if (result.status == BitmapResult.STATUS_EXCEPTION) { 412 mProgressBarNeeded = false; 413 mEmptyText.setText(R.string.failed); 414 mEmptyText.setVisibility(View.VISIBLE); 415 mCallback.onFragmentPhotoLoadComplete(this, false /* success */); 416 } else { 417 bindPhoto(data); 418 mCallback.onFragmentPhotoLoadComplete(this, true /* success */); 419 } 420 } 421 422 /** 423 * Binds an image to the photo view. 424 */ 425 private void bindPhoto(Bitmap bitmap) { 426 if (bitmap != null) { 427 if (mPhotoView != null) { 428 mPhotoView.bindPhoto(bitmap); 429 } 430 enableImageTransforms(true); 431 mPhotoPreviewAndProgress.setVisibility(View.GONE); 432 mProgressBarNeeded = false; 433 } 434 } 435 436 /** 437 * Enable or disable image transformations. When transformations are enabled, this view 438 * consumes all touch events. 439 */ 440 public void enableImageTransforms(boolean enable) { 441 mPhotoView.enableImageTransforms(enable); 442 } 443 444 /** 445 * Resets the photo view to it's default state w/ no bound photo. 446 */ 447 private void resetPhotoView() { 448 if (mPhotoView != null) { 449 mPhotoView.bindPhoto(null); 450 } 451 } 452 453 @Override 454 public void onLoaderReset(Loader<BitmapResult> loader) { 455 // Do nothing 456 } 457 458 @Override 459 public void onClick(View v) { 460 mCallback.toggleFullScreen(); 461 } 462 463 @Override 464 public void onFullScreenChanged(boolean fullScreen) { 465 setViewVisibility(); 466 } 467 468 @Override 469 public void onViewActivated() { 470 if (!mCallback.isFragmentActive(this)) { 471 // we're not in the foreground; reset our view 472 resetViews(); 473 } else { 474 if (!isPhotoBound()) { 475 // Restart the loader 476 getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, 477 null, this); 478 } 479 mCallback.onFragmentVisible(this); 480 } 481 } 482 483 /** 484 * Reset the views to their default states 485 */ 486 public void resetViews() { 487 if (mPhotoView != null) { 488 mPhotoView.resetTransformations(); 489 } 490 } 491 492 @Override 493 public boolean onInterceptMoveLeft(float origX, float origY) { 494 if (!mCallback.isFragmentActive(this)) { 495 // we're not in the foreground; don't intercept any touches 496 return false; 497 } 498 499 return (mPhotoView != null && mPhotoView.interceptMoveLeft(origX, origY)); 500 } 501 502 @Override 503 public boolean onInterceptMoveRight(float origX, float origY) { 504 if (!mCallback.isFragmentActive(this)) { 505 // we're not in the foreground; don't intercept any touches 506 return false; 507 } 508 509 return (mPhotoView != null && mPhotoView.interceptMoveRight(origX, origY)); 510 } 511 512 /** 513 * Returns {@code true} if a photo has been bound. Otherwise, returns {@code false}. 514 */ 515 public boolean isPhotoBound() { 516 return (mPhotoView != null && mPhotoView.isPhotoBound()); 517 } 518 519 /** 520 * Sets view visibility depending upon whether or not we're in "full screen" mode. 521 */ 522 private void setViewVisibility() { 523 final boolean fullScreen = mCallback == null ? false : mCallback.isFragmentFullScreen(this); 524 setFullScreen(fullScreen); 525 } 526 527 /** 528 * Sets full-screen mode for the views. 529 */ 530 public void setFullScreen(boolean fullScreen) { 531 mFullScreen = fullScreen; 532 } 533 534 @Override 535 public void onCursorChanged(Cursor cursor) { 536 if (mAdapter == null) { 537 // The adapter is set in onAttach(), and is guaranteed to be non-null. We have magically 538 // received an onCursorChanged without attaching to an activity. Ignore this cursor 539 // change. 540 return; 541 } 542 // FLAG: There is a problem here: 543 // If the cursor changes, and new items are added at an earlier position than 544 // the current item, we will switch photos here. Really we should probably 545 // try to find a photo with the same url and move the cursor to that position. 546 if (cursor.moveToPosition(mPosition) && !isPhotoBound()) { 547 mCallback.onCursorChanged(this, cursor); 548 549 final LoaderManager manager = getLoaderManager(); 550 551 final Loader<BitmapResult> fakePhotoLoader = manager.getLoader( 552 PhotoViewCallbacks.BITMAP_LOADER_PHOTO); 553 if (fakePhotoLoader != null) { 554 final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakePhotoLoader; 555 mResolvedPhotoUri = mAdapter.getPhotoUri(cursor); 556 loader.setPhotoUri(mResolvedPhotoUri); 557 loader.forceLoad(); 558 } 559 560 if (!mThumbnailShown) { 561 final Loader<BitmapResult> fakeThumbnailLoader = manager.getLoader( 562 PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL); 563 if (fakeThumbnailLoader != null) { 564 final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakeThumbnailLoader; 565 mThumbnailUri = mAdapter.getThumbnailUri(cursor); 566 loader.setPhotoUri(mThumbnailUri); 567 loader.forceLoad(); 568 } 569 } 570 } 571 } 572 573 public int getPosition() { 574 return mPosition; 575 } 576 577 public ProgressBarWrapper getPhotoProgressBar() { 578 return mPhotoProgressBar; 579 } 580 581 public TextView getEmptyText() { 582 return mEmptyText; 583 } 584 585 public ImageView getRetryButton() { 586 return mRetryButton; 587 } 588 589 public boolean isProgressBarNeeded() { 590 return mProgressBarNeeded; 591 } 592 593 private class InternetStateBroadcastReceiver extends BroadcastReceiver { 594 595 @Override 596 public void onReceive(Context context, Intent intent) { 597 // This is only created if we have the correct permissions, so 598 ConnectivityManager connectivityManager = (ConnectivityManager) 599 context.getSystemService(Context.CONNECTIVITY_SERVICE); 600 NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); 601 if (activeNetInfo == null || !activeNetInfo.isConnected()) { 602 mConnected = false; 603 return; 604 } 605 if (mConnected == false && !isPhotoBound()) { 606 if (mThumbnailShown == false) { 607 getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, 608 null, PhotoViewFragment.this); 609 } 610 getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO, 611 null, PhotoViewFragment.this); 612 mConnected = true; 613 mPhotoProgressBar.setVisibility(View.VISIBLE); 614 } 615 } 616 } 617 } 618