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