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