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 androidx.fragment.app.Fragment;
30 import androidx.loader.app.LoaderManager;
31 import androidx.loader.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     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 */
PhotoViewFragment()135     public PhotoViewFragment() {
136         // Do nothing.
137     }
138 
139     /**
140      * Create a {@link PhotoViewFragment}.
141      * @param intent
142      * @param position
143      * @param onlyShowSpinner
144      */
newInstance( Intent intent, int position, boolean onlyShowSpinner)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 
initializeArguments( Intent intent, int position, boolean onlyShowSpinner, PhotoViewFragment f)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
onActivityCreated(Bundle savedInstanceState)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 
getCallbacks()177     protected PhotoViewCallbacks getCallbacks() {
178         return ((ActivityInterface) getActivity()).getController();
179     }
180 
181     @Override
onDetach()182     public void onDetach() {
183         mCallback = null;
184         super.onDetach();
185     }
186 
187     @Override
onCreate(Bundle savedInstanceState)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 
initializeView(View view)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
onResume()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
onPause()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
onDestroyView()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 
getPhotoUri()309     public String getPhotoUri() {
310         return mResolvedPhotoUri;
311     }
312 
313     @Override
onSaveInstanceState(Bundle outState)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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<BitmapResult> loader, BitmapResult result)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 
displayPhoto(BitmapResult result)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      */
bindPhoto(Drawable drawable)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 
getDrawable()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      */
enableImageTransforms(boolean enable)433     public void enableImageTransforms(boolean enable) {
434         mPhotoView.enableImageTransforms(enable);
435     }
436 
437     @Override
onLoaderReset(Loader<BitmapResult> loader)438     public void onLoaderReset(Loader<BitmapResult> loader) {
439         // Do nothing
440     }
441 
442     @Override
onClick(View v)443     public void onClick(View v) {
444         mCallback.toggleFullScreen();
445     }
446 
447     @Override
onFullScreenChanged(boolean fullScreen)448     public void onFullScreenChanged(boolean fullScreen) {
449         setViewVisibility();
450     }
451 
452     @Override
onViewUpNext()453     public void onViewUpNext() {
454         resetViews();
455     }
456 
457     @Override
onViewActivated()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      */
resetViews()475     public void resetViews() {
476         if (mPhotoView != null) {
477             mPhotoView.resetTransformations();
478         }
479     }
480 
481     @Override
onInterceptMoveLeft(float origX, float origY)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
onInterceptMoveRight(float origX, float origY)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      */
isPhotoBound()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      */
setViewVisibility()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      */
setFullScreen(boolean fullScreen)519     public void setFullScreen(boolean fullScreen) {
520         mFullScreen = fullScreen;
521     }
522 
523     @Override
onCursorChanged(Cursor cursor)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 
getPosition()562     public int getPosition() {
563         return mPosition;
564     }
565 
getPhotoProgressBar()566     public ProgressBarWrapper getPhotoProgressBar() {
567         return mPhotoProgressBar;
568     }
569 
getEmptyText()570     public TextView getEmptyText() {
571         return mEmptyText;
572     }
573 
getRetryButton()574     public ImageView getRetryButton() {
575         return mRetryButton;
576     }
577 
isProgressBarNeeded()578     public boolean isProgressBarNeeded() {
579         return mProgressBarNeeded;
580     }
581 
582     private class InternetStateBroadcastReceiver extends BroadcastReceiver {
583 
584         @Override
onReceive(Context context, Intent intent)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