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