• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
interceptMoveLeft(float origX, float origY)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          */
interceptMoveRight(float origX, float origY)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 */
PhotoViewFragment()134     public PhotoViewFragment() {
135         // Do nothing.
136     }
137 
138     /**
139      * Create a {@link PhotoViewFragment}.
140      * @param intent
141      * @param position
142      * @param onlyShowSpinner
143      */
newInstance( Intent intent, int position, boolean onlyShowSpinner)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 
initializeArguments( Intent intent, int position, boolean onlyShowSpinner, PhotoViewFragment f)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
onActivityCreated(Bundle savedInstanceState)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 
getCallbacks()176     protected PhotoViewCallbacks getCallbacks() {
177         return ((ActivityInterface) getActivity()).getController();
178     }
179 
180     @Override
onDetach()181     public void onDetach() {
182         mCallback = null;
183         super.onDetach();
184     }
185 
186     @Override
onCreate(Bundle savedInstanceState)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 
initializeView(View view)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
onResume()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
onPause()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
onDestroyView()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 
getPhotoUri()307     public String getPhotoUri() {
308         return mResolvedPhotoUri;
309     }
310 
311     @Override
onSaveInstanceState(Bundle outState)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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<BitmapResult> loader, BitmapResult result)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 
displayPhoto(BitmapResult result)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      */
bindPhoto(Drawable drawable)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 
getDrawable()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      */
enableImageTransforms(boolean enable)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      */
resetPhotoView()437     private void resetPhotoView() {
438         if (mPhotoView != null) {
439             mPhotoView.bindPhoto(null);
440         }
441     }
442 
443     @Override
onLoaderReset(Loader<BitmapResult> loader)444     public void onLoaderReset(Loader<BitmapResult> loader) {
445         // Do nothing
446     }
447 
448     @Override
onClick(View v)449     public void onClick(View v) {
450         mCallback.toggleFullScreen();
451     }
452 
453     @Override
onFullScreenChanged(boolean fullScreen)454     public void onFullScreenChanged(boolean fullScreen) {
455         setViewVisibility();
456     }
457 
458     @Override
onViewUpNext()459     public void onViewUpNext() {
460         resetViews();
461     }
462 
463     @Override
onViewActivated()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      */
resetViews()481     public void resetViews() {
482         if (mPhotoView != null) {
483             mPhotoView.resetTransformations();
484         }
485     }
486 
487     @Override
onInterceptMoveLeft(float origX, float origY)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
onInterceptMoveRight(float origX, float origY)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      */
isPhotoBound()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      */
setViewVisibility()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      */
setFullScreen(boolean fullScreen)525     public void setFullScreen(boolean fullScreen) {
526         mFullScreen = fullScreen;
527     }
528 
529     @Override
onCursorChanged(Cursor cursor)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 
getPosition()568     public int getPosition() {
569         return mPosition;
570     }
571 
getPhotoProgressBar()572     public ProgressBarWrapper getPhotoProgressBar() {
573         return mPhotoProgressBar;
574     }
575 
getEmptyText()576     public TextView getEmptyText() {
577         return mEmptyText;
578     }
579 
getRetryButton()580     public ImageView getRetryButton() {
581         return mRetryButton;
582     }
583 
isProgressBarNeeded()584     public boolean isProgressBarNeeded() {
585         return mProgressBarNeeded;
586     }
587 
588     private class InternetStateBroadcastReceiver extends BroadcastReceiver {
589 
590         @Override
onReceive(Context context, Intent intent)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