Home | History | Annotate | Download | only in fragments
      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