/* * Copyright (C) 2011 Google Inc. * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ex.photo.fragments; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.android.ex.photo.Intents; import com.android.ex.photo.PhotoViewCallbacks; import com.android.ex.photo.PhotoViewCallbacks.CursorChangedListener; import com.android.ex.photo.PhotoViewCallbacks.OnScreenListener; import com.android.ex.photo.PhotoViewController.ActivityInterface; import com.android.ex.photo.R; import com.android.ex.photo.adapters.PhotoPagerAdapter; import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface; import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; import com.android.ex.photo.views.PhotoView; import com.android.ex.photo.views.ProgressBarWrapper; /** * Displays a photo. */ public class PhotoViewFragment extends Fragment implements LoaderManager.LoaderCallbacks, OnClickListener, OnScreenListener, CursorChangedListener { /** * Interface for components that are internally scrollable left-to-right. */ public static interface HorizontallyScrollable { /** * Return {@code true} if the component needs to receive right-to-left * touch movements. * * @param origX the raw x coordinate of the initial touch * @param origY the raw y coordinate of the initial touch */ public boolean interceptMoveLeft(float origX, float origY); /** * Return {@code true} if the component needs to receive left-to-right * touch movements. * * @param origX the raw x coordinate of the initial touch * @param origY the raw y coordinate of the initial touch */ public boolean interceptMoveRight(float origX, float origY); } protected final static String STATE_INTENT_KEY = "com.android.mail.photo.fragments.PhotoViewFragment.INTENT"; protected final static String ARG_INTENT = "arg-intent"; protected final static String ARG_POSITION = "arg-position"; protected final static String ARG_SHOW_SPINNER = "arg-show-spinner"; /** The URL of a photo to display */ protected String mResolvedPhotoUri; protected String mThumbnailUri; /** The intent we were launched with */ protected Intent mIntent; protected PhotoViewCallbacks mCallback; protected PhotoPagerAdapter mAdapter; protected BroadcastReceiver mInternetStateReceiver; protected PhotoView mPhotoView; protected ImageView mPhotoPreviewImage; protected TextView mEmptyText; protected ImageView mRetryButton; protected ProgressBarWrapper mPhotoProgressBar; protected int mPosition; /** Whether or not the fragment should make the photo full-screen */ protected boolean mFullScreen; /** * True if the PhotoViewFragment should watch the network state in order to restart loaders. */ protected boolean mWatchNetworkState; /** Whether or not this fragment will only show the loading spinner */ protected boolean mOnlyShowSpinner; /** Whether or not the progress bar is showing valid information about the progress stated */ protected boolean mProgressBarNeeded = true; protected View mPhotoPreviewAndProgress; protected boolean mThumbnailShown; /** Whether or not there is currently a connection to the internet */ protected boolean mConnected; /** Whether or not we can display the thumbnail at fullscreen size */ protected boolean mDisplayThumbsFullScreen; /** Public no-arg constructor for allowing the framework to handle orientation changes */ public PhotoViewFragment() { // Do nothing. } /** * Create a {@link PhotoViewFragment}. * @param intent * @param position * @param onlyShowSpinner */ public static PhotoViewFragment newInstance( Intent intent, int position, boolean onlyShowSpinner) { final PhotoViewFragment f = new PhotoViewFragment(); initializeArguments(intent, position, onlyShowSpinner, f); return f; } public static void initializeArguments( Intent intent, int position, boolean onlyShowSpinner, PhotoViewFragment f) { final Bundle b = new Bundle(); b.putParcelable(ARG_INTENT, intent); b.putInt(ARG_POSITION, position); b.putBoolean(ARG_SHOW_SPINNER, onlyShowSpinner); f.setArguments(b); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mCallback = getCallbacks(); if (mCallback == null) { throw new IllegalArgumentException( "Activity must be a derived class of PhotoViewActivity"); } mAdapter = mCallback.getAdapter(); if (mAdapter == null) { throw new IllegalStateException("Callback reported null adapter"); } // Don't call until we've setup the entire view setViewVisibility(); } protected PhotoViewCallbacks getCallbacks() { return ((ActivityInterface) getActivity()).getController(); } @Override public void onDetach() { mCallback = null; super.onDetach(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Bundle bundle = getArguments(); if (bundle == null) { return; } mIntent = bundle.getParcelable(ARG_INTENT); mDisplayThumbsFullScreen = mIntent.getBooleanExtra( Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false); mPosition = bundle.getInt(ARG_POSITION); mOnlyShowSpinner = bundle.getBoolean(ARG_SHOW_SPINNER); mProgressBarNeeded = true; if (savedInstanceState != null) { final Bundle state = savedInstanceState.getBundle(STATE_INTENT_KEY); if (state != null) { mIntent = new Intent().putExtras(state); } } if (mIntent != null) { mResolvedPhotoUri = mIntent.getStringExtra(Intents.EXTRA_RESOLVED_PHOTO_URI); mThumbnailUri = mIntent.getStringExtra(Intents.EXTRA_THUMBNAIL_URI); mWatchNetworkState = mIntent.getBooleanExtra(Intents.EXTRA_WATCH_NETWORK, false); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.photo_fragment_view, container, false); initializeView(view); return view; } protected void initializeView(View view) { mPhotoView = (PhotoView) view.findViewById(R.id.photo_view); mPhotoView.setMaxInitialScale(mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1)); mPhotoView.setOnClickListener(this); mPhotoView.setFullScreen(mFullScreen, false); mPhotoView.enableImageTransforms(false); mPhotoPreviewAndProgress = view.findViewById(R.id.photo_preview); mPhotoPreviewImage = (ImageView) view.findViewById(R.id.photo_preview_image); mThumbnailShown = false; final ProgressBar indeterminate = (ProgressBar) view.findViewById(R.id.indeterminate_progress); final ProgressBar determinate = (ProgressBar) view.findViewById(R.id.determinate_progress); mPhotoProgressBar = new ProgressBarWrapper(determinate, indeterminate, true); mEmptyText = (TextView) view.findViewById(R.id.empty_text); mRetryButton = (ImageView) view.findViewById(R.id.retry_button); // Don't call until we've setup the entire view setViewVisibility(); } @Override public void onResume() { super.onResume(); mCallback.addScreenListener(mPosition, this); mCallback.addCursorListener(this); if (mWatchNetworkState) { if (mInternetStateReceiver == null) { mInternetStateReceiver = new InternetStateBroadcastReceiver(); } getActivity().registerReceiver(mInternetStateReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); ConnectivityManager connectivityManager = (ConnectivityManager) getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); if (activeNetInfo != null) { mConnected = activeNetInfo.isConnected(); } else { // Best to set this to false, since it won't stop us from trying to download, // only allow us to try re-download if we get notified that we do have a connection. mConnected = false; } } if (!isPhotoBound()) { mProgressBarNeeded = true; mPhotoPreviewAndProgress.setVisibility(View.VISIBLE); getLoaderManager().initLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, null, this); // FLAG: If we are displaying thumbnails at fullscreen size, then we // could defer the loading of the fullscreen image until the thumbnail // has finished loading, or even until the user attempts to zoom in. getLoaderManager().initLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO, null, this); } } @Override public void onPause() { // Remove listeners if (mWatchNetworkState) { getActivity().unregisterReceiver(mInternetStateReceiver); } mCallback.removeCursorListener(this); mCallback.removeScreenListener(mPosition); resetPhotoView(); super.onPause(); } @Override public void onDestroyView() { // Clean up views and other components if (mPhotoView != null) { mPhotoView.clear(); mPhotoView = null; } super.onDestroyView(); } public String getPhotoUri() { return mResolvedPhotoUri; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mIntent != null) { outState.putParcelable(STATE_INTENT_KEY, mIntent.getExtras()); } } @Override public Loader onCreateLoader(int id, Bundle args) { if(mOnlyShowSpinner) { return null; } String uri = null; switch (id) { case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: uri = mThumbnailUri; break; case PhotoViewCallbacks.BITMAP_LOADER_PHOTO: uri = mResolvedPhotoUri; break; } return mCallback.onCreateBitmapLoader(id, args, uri); } @Override public void onLoadFinished(Loader loader, BitmapResult result) { // If we don't have a view, the fragment has been paused. We'll get the cursor again later. // If we're not added, the fragment has detached during the loading process. We no longer // need the result. if (getView() == null || !isAdded()) { return; } final Drawable data = result.getDrawable(getResources()); final int id = loader.getId(); switch (id) { case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: if (mDisplayThumbsFullScreen) { displayPhoto(result); } else { if (isPhotoBound()) { // There is need to do anything with the thumbnail // image, as the full size image is being shown. return; } if (data == null) { // no preview, show default mPhotoPreviewImage.setImageResource(R.drawable.default_image); mThumbnailShown = false; } else { // show preview mPhotoPreviewImage.setImageDrawable(data); mThumbnailShown = true; } mPhotoPreviewImage.setVisibility(View.VISIBLE); if (getResources().getBoolean(R.bool.force_thumbnail_no_scaling)) { mPhotoPreviewImage.setScaleType(ImageView.ScaleType.CENTER); } enableImageTransforms(false); } break; case PhotoViewCallbacks.BITMAP_LOADER_PHOTO: displayPhoto(result); break; default: break; } if (mProgressBarNeeded == false) { // Hide the progress bar as it isn't needed anymore. mPhotoProgressBar.setVisibility(View.GONE); } if (data != null) { mCallback.onNewPhotoLoaded(mPosition); } setViewVisibility(); } private void displayPhoto(BitmapResult result) { if (result.status == BitmapResult.STATUS_EXCEPTION) { mProgressBarNeeded = false; mEmptyText.setText(R.string.failed); mEmptyText.setVisibility(View.VISIBLE); mCallback.onFragmentPhotoLoadComplete(this, false /* success */); } else { mEmptyText.setVisibility(View.GONE); final Drawable data = result.getDrawable(getResources()); bindPhoto(data); mCallback.onFragmentPhotoLoadComplete(this, true /* success */); } } /** * Binds an image to the photo view. */ private void bindPhoto(Drawable drawable) { if (drawable != null) { if (mPhotoView != null) { mPhotoView.bindDrawable(drawable); } enableImageTransforms(true); mPhotoPreviewAndProgress.setVisibility(View.GONE); mProgressBarNeeded = false; } } public Drawable getDrawable() { return (mPhotoView != null ? mPhotoView.getDrawable() : null); } /** * Enable or disable image transformations. When transformations are enabled, this view * consumes all touch events. */ public void enableImageTransforms(boolean enable) { mPhotoView.enableImageTransforms(enable); } /** * Resets the photo view to it's default state w/ no bound photo. */ private void resetPhotoView() { if (mPhotoView != null) { mPhotoView.bindPhoto(null); } } @Override public void onLoaderReset(Loader loader) { // Do nothing } @Override public void onClick(View v) { mCallback.toggleFullScreen(); } @Override public void onFullScreenChanged(boolean fullScreen) { setViewVisibility(); } @Override public void onViewUpNext() { resetViews(); } @Override public void onViewActivated() { if (!mCallback.isFragmentActive(this)) { // we're not in the foreground; reset our view resetViews(); } else { if (!isPhotoBound()) { // Restart the loader getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, null, this); } mCallback.onFragmentVisible(this); } } /** * Reset the views to their default states */ public void resetViews() { if (mPhotoView != null) { mPhotoView.resetTransformations(); } } @Override public boolean onInterceptMoveLeft(float origX, float origY) { if (!mCallback.isFragmentActive(this)) { // we're not in the foreground; don't intercept any touches return false; } return (mPhotoView != null && mPhotoView.interceptMoveLeft(origX, origY)); } @Override public boolean onInterceptMoveRight(float origX, float origY) { if (!mCallback.isFragmentActive(this)) { // we're not in the foreground; don't intercept any touches return false; } return (mPhotoView != null && mPhotoView.interceptMoveRight(origX, origY)); } /** * Returns {@code true} if a photo has been bound. Otherwise, returns {@code false}. */ public boolean isPhotoBound() { return (mPhotoView != null && mPhotoView.isPhotoBound()); } /** * Sets view visibility depending upon whether or not we're in "full screen" mode. */ private void setViewVisibility() { final boolean fullScreen = mCallback == null ? false : mCallback.isFragmentFullScreen(this); setFullScreen(fullScreen); } /** * Sets full-screen mode for the views. */ public void setFullScreen(boolean fullScreen) { mFullScreen = fullScreen; } @Override public void onCursorChanged(Cursor cursor) { if (mAdapter == null) { // The adapter is set in onAttach(), and is guaranteed to be non-null. We have magically // received an onCursorChanged without attaching to an activity. Ignore this cursor // change. return; } // FLAG: There is a problem here: // If the cursor changes, and new items are added at an earlier position than // the current item, we will switch photos here. Really we should probably // try to find a photo with the same url and move the cursor to that position. if (cursor.moveToPosition(mPosition) && !isPhotoBound()) { mCallback.onCursorChanged(this, cursor); final LoaderManager manager = getLoaderManager(); final Loader fakePhotoLoader = manager.getLoader( PhotoViewCallbacks.BITMAP_LOADER_PHOTO); if (fakePhotoLoader != null) { final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakePhotoLoader; mResolvedPhotoUri = mAdapter.getPhotoUri(cursor); loader.setPhotoUri(mResolvedPhotoUri); loader.forceLoad(); } if (!mThumbnailShown) { final Loader fakeThumbnailLoader = manager.getLoader( PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL); if (fakeThumbnailLoader != null) { final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakeThumbnailLoader; mThumbnailUri = mAdapter.getThumbnailUri(cursor); loader.setPhotoUri(mThumbnailUri); loader.forceLoad(); } } } } public int getPosition() { return mPosition; } public ProgressBarWrapper getPhotoProgressBar() { return mPhotoProgressBar; } public TextView getEmptyText() { return mEmptyText; } public ImageView getRetryButton() { return mRetryButton; } public boolean isProgressBarNeeded() { return mProgressBarNeeded; } private class InternetStateBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // This is only created if we have the correct permissions, so ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); if (activeNetInfo == null || !activeNetInfo.isConnected()) { mConnected = false; return; } if (mConnected == false && !isPhotoBound()) { if (mThumbnailShown == false) { getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, null, PhotoViewFragment.this); } getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO, null, PhotoViewFragment.this); mConnected = true; mPhotoProgressBar.setVisibility(View.VISIBLE); } } } }