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
18package com.android.ex.photo.fragments;
19
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.database.Cursor;
25import android.graphics.drawable.Drawable;
26import android.net.ConnectivityManager;
27import android.net.NetworkInfo;
28import android.os.Bundle;
29import android.support.v4.app.Fragment;
30import android.support.v4.app.LoaderManager;
31import android.support.v4.content.Loader;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.View.OnClickListener;
35import android.view.ViewGroup;
36import android.widget.ImageView;
37import android.widget.ProgressBar;
38import android.widget.TextView;
39
40import com.android.ex.photo.Intents;
41import com.android.ex.photo.PhotoViewCallbacks;
42import com.android.ex.photo.PhotoViewCallbacks.CursorChangedListener;
43import com.android.ex.photo.PhotoViewCallbacks.OnScreenListener;
44import com.android.ex.photo.PhotoViewController.ActivityInterface;
45import com.android.ex.photo.R;
46import com.android.ex.photo.adapters.PhotoPagerAdapter;
47import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface;
48import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
49import com.android.ex.photo.views.PhotoView;
50import com.android.ex.photo.views.ProgressBarWrapper;
51
52/**
53 * Displays a photo.
54 */
55public 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