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