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