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