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             final Drawable data = result.getDrawable(getResources());
    403             bindPhoto(data);
    404             mCallback.onFragmentPhotoLoadComplete(this, true /* success */);
    405         }
    406     }
    407 
    408     /**
    409      * Binds an image to the photo view.
    410      */
    411     private void bindPhoto(Drawable drawable) {
    412         if (drawable != null) {
    413             if (mPhotoView != null) {
    414                 mPhotoView.bindDrawable(drawable);
    415             }
    416             enableImageTransforms(true);
    417             mPhotoPreviewAndProgress.setVisibility(View.GONE);
    418             mProgressBarNeeded = false;
    419         }
    420     }
    421 
    422     public Drawable getDrawable() {
    423         return (mPhotoView != null ? mPhotoView.getDrawable() : null);
    424     }
    425 
    426     /**
    427      * Enable or disable image transformations. When transformations are enabled, this view
    428      * consumes all touch events.
    429      */
    430     public void enableImageTransforms(boolean enable) {
    431         mPhotoView.enableImageTransforms(enable);
    432     }
    433 
    434     /**
    435      * Resets the photo view to it's default state w/ no bound photo.
    436      */
    437     private void resetPhotoView() {
    438         if (mPhotoView != null) {
    439             mPhotoView.bindPhoto(null);
    440         }
    441     }
    442 
    443     @Override
    444     public void onLoaderReset(Loader<BitmapResult> loader) {
    445         // Do nothing
    446     }
    447 
    448     @Override
    449     public void onClick(View v) {
    450         mCallback.toggleFullScreen();
    451     }
    452 
    453     @Override
    454     public void onFullScreenChanged(boolean fullScreen) {
    455         setViewVisibility();
    456     }
    457 
    458     @Override
    459     public void onViewUpNext() {
    460         resetViews();
    461     }
    462 
    463     @Override
    464     public void onViewActivated() {
    465         if (!mCallback.isFragmentActive(this)) {
    466             // we're not in the foreground; reset our view
    467             resetViews();
    468         } else {
    469             if (!isPhotoBound()) {
    470                 // Restart the loader
    471                 getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
    472                         null, this);
    473             }
    474             mCallback.onFragmentVisible(this);
    475         }
    476     }
    477 
    478     /**
    479      * Reset the views to their default states
    480      */
    481     public void resetViews() {
    482         if (mPhotoView != null) {
    483             mPhotoView.resetTransformations();
    484         }
    485     }
    486 
    487     @Override
    488     public boolean onInterceptMoveLeft(float origX, float origY) {
    489         if (!mCallback.isFragmentActive(this)) {
    490             // we're not in the foreground; don't intercept any touches
    491             return false;
    492         }
    493 
    494         return (mPhotoView != null && mPhotoView.interceptMoveLeft(origX, origY));
    495     }
    496 
    497     @Override
    498     public boolean onInterceptMoveRight(float origX, float origY) {
    499         if (!mCallback.isFragmentActive(this)) {
    500             // we're not in the foreground; don't intercept any touches
    501             return false;
    502         }
    503 
    504         return (mPhotoView != null && mPhotoView.interceptMoveRight(origX, origY));
    505     }
    506 
    507     /**
    508      * Returns {@code true} if a photo has been bound. Otherwise, returns {@code false}.
    509      */
    510     public boolean isPhotoBound() {
    511         return (mPhotoView != null && mPhotoView.isPhotoBound());
    512     }
    513 
    514     /**
    515      * Sets view visibility depending upon whether or not we're in "full screen" mode.
    516      */
    517     private void setViewVisibility() {
    518         final boolean fullScreen = mCallback == null ? false : mCallback.isFragmentFullScreen(this);
    519         setFullScreen(fullScreen);
    520     }
    521 
    522     /**
    523      * Sets full-screen mode for the views.
    524      */
    525     public void setFullScreen(boolean fullScreen) {
    526         mFullScreen = fullScreen;
    527     }
    528 
    529     @Override
    530     public void onCursorChanged(Cursor cursor) {
    531         if (mAdapter == null) {
    532             // The adapter is set in onAttach(), and is guaranteed to be non-null. We have magically
    533             // received an onCursorChanged without attaching to an activity. Ignore this cursor
    534             // change.
    535             return;
    536         }
    537         // FLAG: There is a problem here:
    538         // If the cursor changes, and new items are added at an earlier position than
    539         // the current item, we will switch photos here. Really we should probably
    540         // try to find a photo with the same url and move the cursor to that position.
    541         if (cursor.moveToPosition(mPosition) && !isPhotoBound()) {
    542             mCallback.onCursorChanged(this, cursor);
    543 
    544             final LoaderManager manager = getLoaderManager();
    545 
    546             final Loader<BitmapResult> fakePhotoLoader = manager.getLoader(
    547                     PhotoViewCallbacks.BITMAP_LOADER_PHOTO);
    548             if (fakePhotoLoader != null) {
    549                 final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakePhotoLoader;
    550                 mResolvedPhotoUri = mAdapter.getPhotoUri(cursor);
    551                 loader.setPhotoUri(mResolvedPhotoUri);
    552                 loader.forceLoad();
    553             }
    554 
    555             if (!mThumbnailShown) {
    556                 final Loader<BitmapResult> fakeThumbnailLoader = manager.getLoader(
    557                         PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL);
    558                 if (fakeThumbnailLoader != null) {
    559                     final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakeThumbnailLoader;
    560                     mThumbnailUri = mAdapter.getThumbnailUri(cursor);
    561                     loader.setPhotoUri(mThumbnailUri);
    562                     loader.forceLoad();
    563                 }
    564             }
    565         }
    566     }
    567 
    568     public int getPosition() {
    569         return mPosition;
    570     }
    571 
    572     public ProgressBarWrapper getPhotoProgressBar() {
    573         return mPhotoProgressBar;
    574     }
    575 
    576     public TextView getEmptyText() {
    577         return mEmptyText;
    578     }
    579 
    580     public ImageView getRetryButton() {
    581         return mRetryButton;
    582     }
    583 
    584     public boolean isProgressBarNeeded() {
    585         return mProgressBarNeeded;
    586     }
    587 
    588     private class InternetStateBroadcastReceiver extends BroadcastReceiver {
    589 
    590         @Override
    591         public void onReceive(Context context, Intent intent) {
    592             // This is only created if we have the correct permissions, so
    593             ConnectivityManager connectivityManager = (ConnectivityManager)
    594                     context.getSystemService(Context.CONNECTIVITY_SERVICE);
    595             NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
    596             if (activeNetInfo == null || !activeNetInfo.isConnected()) {
    597                 mConnected = false;
    598                 return;
    599             }
    600             if (mConnected == false && !isPhotoBound()) {
    601                 if (mThumbnailShown == false) {
    602                     getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
    603                             null, PhotoViewFragment.this);
    604                 }
    605                 getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO,
    606                         null, PhotoViewFragment.this);
    607                 mConnected = true;
    608                 mPhotoProgressBar.setVisibility(View.VISIBLE);
    609             }
    610         }
    611     }
    612 }
    613