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.app.Activity;
21import android.content.Context;
22import android.content.Intent;
23import android.database.Cursor;
24import android.graphics.Bitmap;
25import android.os.Bundle;
26import android.os.Handler;
27import android.support.v4.app.Fragment;
28import android.support.v4.app.LoaderManager;
29import android.support.v4.content.Loader;
30import android.util.DisplayMetrics;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.View.OnClickListener;
34import android.view.ViewGroup;
35import android.view.WindowManager;
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.R;
45import com.android.ex.photo.adapters.PhotoPagerAdapter;
46import com.android.ex.photo.loaders.PhotoBitmapLoader;
47import com.android.ex.photo.util.ImageUtils;
48import com.android.ex.photo.views.PhotoView;
49import com.android.ex.photo.views.ProgressBarWrapper;
50
51/**
52 * Displays a photo.
53 */
54public class PhotoViewFragment extends Fragment implements
55        LoaderManager.LoaderCallbacks<Bitmap>, OnClickListener, OnScreenListener, CursorChangedListener {
56    /**
57     * Interface for components that are internally scrollable left-to-right.
58     */
59    public static interface HorizontallyScrollable {
60        /**
61         * Return {@code true} if the component needs to receive right-to-left
62         * touch movements.
63         *
64         * @param origX the raw x coordinate of the initial touch
65         * @param origY the raw y coordinate of the initial touch
66         */
67
68        public boolean interceptMoveLeft(float origX, float origY);
69
70        /**
71         * Return {@code true} if the component needs to receive left-to-right
72         * touch movements.
73         *
74         * @param origX the raw x coordinate of the initial touch
75         * @param origY the raw y coordinate of the initial touch
76         */
77        public boolean interceptMoveRight(float origX, float origY);
78    }
79
80    protected final static String STATE_INTENT_KEY =
81            "com.android.mail.photo.fragments.PhotoViewFragment.INTENT";
82
83    // Loader IDs
84    protected final static int LOADER_ID_PHOTO = 1;
85    protected final static int LOADER_ID_THUMBNAIL = 2;
86
87    /** The size of the photo */
88    public static Integer sPhotoSize;
89
90    /** The URL of a photo to display */
91    protected String mResolvedPhotoUri;
92    protected String mThumbnailUri;
93    /** The intent we were launched with */
94    protected Intent mIntent;
95    protected PhotoViewCallbacks mCallback;
96    protected PhotoPagerAdapter mAdapter;
97
98    protected PhotoView mPhotoView;
99    protected ImageView mPhotoPreviewImage;
100    protected TextView mEmptyText;
101    protected ImageView mRetryButton;
102    protected ProgressBarWrapper mPhotoProgressBar;
103
104    protected final int mPosition;
105
106    /** Whether or not the fragment should make the photo full-screen */
107    protected boolean mFullScreen;
108
109    /** Whether or not this fragment will only show the loading spinner */
110    protected final boolean mOnlyShowSpinner;
111
112    /** Whether or not the progress bar is showing valid information about the progress stated */
113    protected boolean mProgressBarNeeded = true;
114
115    protected View mPhotoPreviewAndProgress;
116
117    public PhotoViewFragment() {
118        mPosition = -1;
119        mOnlyShowSpinner = false;
120        mProgressBarNeeded = true;
121    }
122
123    public PhotoViewFragment(Intent intent, int position, PhotoPagerAdapter adapter,
124            boolean onlyShowSpinner) {
125        mIntent = intent;
126        mPosition = position;
127        mAdapter = adapter;
128        mOnlyShowSpinner = onlyShowSpinner;
129        mProgressBarNeeded = true;
130    }
131
132    @Override
133    public void onAttach(Activity activity) {
134        super.onAttach(activity);
135        mCallback = (PhotoViewCallbacks) activity;
136        if (mCallback == null) {
137            throw new IllegalArgumentException(
138                    "Activity must be a derived class of PhotoViewActivity");
139        }
140
141        if (sPhotoSize == null) {
142            final DisplayMetrics metrics = new DisplayMetrics();
143            final WindowManager wm =
144                    (WindowManager) getActivity().getSystemService(Context.WINDOW_SERVICE);
145            final ImageUtils.ImageSize imageSize = ImageUtils.sUseImageSize;
146            wm.getDefaultDisplay().getMetrics(metrics);
147            switch (imageSize) {
148                case EXTRA_SMALL: {
149                    // Use a photo that's 80% of the "small" size
150                    sPhotoSize = (Math.min(metrics.heightPixels, metrics.widthPixels) * 800) / 1000;
151                    break;
152                }
153
154                case SMALL:
155                case NORMAL:
156                default: {
157                    sPhotoSize = Math.min(metrics.heightPixels, metrics.widthPixels);
158                    break;
159                }
160            }
161        }
162    }
163
164    @Override
165    public void onDetach() {
166        mCallback = null;
167        super.onDetach();
168    }
169
170    @Override
171    public void onCreate(Bundle savedInstanceState) {
172        super.onCreate(savedInstanceState);
173
174        if (savedInstanceState != null) {
175            final Bundle state = savedInstanceState.getBundle(STATE_INTENT_KEY);
176            if (state != null) {
177                mIntent = new Intent().putExtras(state);
178            }
179        }
180
181        if (mIntent != null) {
182            mResolvedPhotoUri = mIntent.getStringExtra(Intents.EXTRA_RESOLVED_PHOTO_URI);
183            mThumbnailUri = mIntent.getStringExtra(Intents.EXTRA_THUMBNAIL_URI);
184        }
185    }
186
187    @Override
188    public View onCreateView(LayoutInflater inflater, ViewGroup container,
189            Bundle savedInstanceState) {
190        final View view = inflater.inflate(R.layout.photo_fragment_view, container, false);
191
192        mPhotoView = (PhotoView) view.findViewById(R.id.photo_view);
193        mPhotoView.setMaxInitialScale(mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1));
194        mPhotoView.setOnClickListener(this);
195        mPhotoView.setFullScreen(mFullScreen, false);
196        mPhotoView.enableImageTransforms(false);
197
198        mPhotoPreviewAndProgress = view.findViewById(R.id.photo_preview);
199        mPhotoPreviewImage = (ImageView) view.findViewById(R.id.photo_preview_image);
200        final ProgressBar indeterminate =
201                (ProgressBar) view.findViewById(R.id.indeterminate_progress);
202        final ProgressBar determinate =
203                (ProgressBar) view.findViewById(R.id.determinate_progress);
204        mPhotoProgressBar = new ProgressBarWrapper(determinate, indeterminate, true);
205        mEmptyText = (TextView) view.findViewById(R.id.empty_text);
206        mRetryButton = (ImageView) view.findViewById(R.id.retry_button);
207
208        // Don't call until we've setup the entire view
209        setViewVisibility();
210
211        return view;
212    }
213
214    @Override
215    public void onResume() {
216        mCallback.addScreenListener(this);
217        mCallback.addCursorListener(this);
218
219        getLoaderManager().initLoader(LOADER_ID_THUMBNAIL, null, this);
220
221        super.onResume();
222    }
223
224    @Override
225    public void onPause() {
226        super.onPause();
227        // Remove listeners
228        mCallback.removeCursorListener(this);
229        mCallback.removeScreenListener(this);
230        resetPhotoView();
231    }
232
233    @Override
234    public void onDestroyView() {
235        // Clean up views and other components
236        if (mPhotoView != null) {
237            mPhotoView.clear();
238            mPhotoView = null;
239        }
240
241        super.onDestroyView();
242    }
243
244    @Override
245    public void onSaveInstanceState(Bundle outState) {
246        super.onSaveInstanceState(outState);
247
248        if (mIntent != null) {
249            outState.putParcelable(STATE_INTENT_KEY, mIntent.getExtras());
250        }
251    }
252
253    @Override
254    public Loader<Bitmap> onCreateLoader(int id, Bundle args) {
255        if(mOnlyShowSpinner) {
256            return null;
257        }
258        switch (id) {
259            case LOADER_ID_PHOTO:
260                return new PhotoBitmapLoader(getActivity(), mResolvedPhotoUri);
261            case LOADER_ID_THUMBNAIL:
262                return new PhotoBitmapLoader(getActivity(), mThumbnailUri);
263            default:
264                return null;
265        }
266    }
267
268    @Override
269    public void onLoadFinished(Loader<Bitmap> loader, Bitmap data) {
270        // If we don't have a view, the fragment has been paused. We'll get the cursor again later.
271        if (getView() == null) {
272            return;
273        }
274
275        final int id = loader.getId();
276        switch (id) {
277            case LOADER_ID_PHOTO:
278                if (data != null) {
279                    bindPhoto(data);
280                    enableImageTransforms(true);
281                    mPhotoPreviewAndProgress.setVisibility(View.GONE);
282                    mProgressBarNeeded = false;
283                } else {
284                    // Received a null result for the full size image.  Instead attempt to load the
285                    // thumbnail
286                    Handler handler = new Handler();
287                    handler.post(new Runnable() {
288                        @Override
289                        public void run() {
290                            getLoaderManager().initLoader(LOADER_ID_THUMBNAIL, null,
291                                                          PhotoViewFragment.this);
292                        }
293                    });
294                }
295                break;
296            case LOADER_ID_THUMBNAIL:
297                mProgressBarNeeded = false;
298                if (isPhotoBound()) {
299                    // There is need to do anything with the thumbnail image, as the full size
300                    // image is being shown.
301                    mPhotoPreviewAndProgress.setVisibility(View.GONE);
302                    return;
303                } else if (data == null) {
304                    // no preview, show default
305                    mPhotoPreviewImage.setVisibility(View.VISIBLE);
306                    mPhotoPreviewImage.setImageResource(R.drawable.default_image);
307                } else {
308                    bindPhoto(data);
309                    enableImageTransforms(false);
310                    Handler handler = new Handler();
311                    handler.post(new Runnable() {
312                        @Override
313                        public void run() {
314                            getLoaderManager().initLoader(LOADER_ID_PHOTO, null,
315                                PhotoViewFragment.this);
316                        }
317                    });
318                }
319                break;
320            default:
321                break;
322        }
323
324        if (mProgressBarNeeded == false) {
325            // Hide the progress bar as it isn't needed anymore.
326            mPhotoProgressBar.setVisibility(View.GONE);
327        }
328
329        mCallback.setViewActivated();
330        setViewVisibility();
331    }
332
333    /**
334     * Binds an image to the photo view.
335     */
336    private void bindPhoto(Bitmap bitmap) {
337        if (mPhotoView != null) {
338            mPhotoView.bindPhoto(bitmap);
339        }
340    }
341
342    /**
343     * Enable or disable image transformations. When transformations are enabled, this view
344     * consumes all touch events.
345     */
346    public void enableImageTransforms(boolean enable) {
347        mPhotoView.enableImageTransforms(enable);
348    }
349
350    /**
351     * Resets the photo view to it's default state w/ no bound photo.
352     */
353    private void resetPhotoView() {
354        if (mPhotoView != null) {
355            mPhotoView.bindPhoto(null);
356        }
357    }
358
359    @Override
360    public void onLoaderReset(Loader<Bitmap> loader) {
361        // Do nothing
362    }
363
364    @Override
365    public void onClick(View v) {
366        mCallback.toggleFullScreen();
367    }
368
369    @Override
370    public void onFullScreenChanged(boolean fullScreen) {
371        setViewVisibility();
372    }
373
374    @Override
375    public void onViewActivated() {
376        if (!mCallback.isFragmentActive(this)) {
377            // we're not in the foreground; reset our view
378            resetViews();
379        } else {
380            mCallback.onFragmentVisible(this);
381        }
382    }
383
384    /**
385     * Reset the views to their default states
386     */
387    public void resetViews() {
388        if (mPhotoView != null) {
389            mPhotoView.resetTransformations();
390        }
391    }
392
393    @Override
394    public boolean onInterceptMoveLeft(float origX, float origY) {
395        if (!mCallback.isFragmentActive(this)) {
396            // we're not in the foreground; don't intercept any touches
397            return false;
398        }
399
400        return (mPhotoView != null && mPhotoView.interceptMoveLeft(origX, origY));
401    }
402
403    @Override
404    public boolean onInterceptMoveRight(float origX, float origY) {
405        if (!mCallback.isFragmentActive(this)) {
406            // we're not in the foreground; don't intercept any touches
407            return false;
408        }
409
410        return (mPhotoView != null && mPhotoView.interceptMoveRight(origX, origY));
411    }
412
413    /**
414     * Returns {@code true} if a photo has been bound. Otherwise, returns {@code false}.
415     */
416    public boolean isPhotoBound() {
417        return (mPhotoView != null && mPhotoView.isPhotoBound());
418    }
419
420    /**
421     * Sets view visibility depending upon whether or not we're in "full screen" mode.
422     */
423    private void setViewVisibility() {
424        final boolean fullScreen = mCallback.isFragmentFullScreen(this);
425        final boolean hide = fullScreen;
426
427        setFullScreen(hide);
428    }
429
430    /**
431     * Sets full-screen mode for the views.
432     */
433    public void setFullScreen(boolean fullScreen) {
434        mFullScreen = fullScreen;
435    }
436
437    @Override
438    public void onCursorChanged(Cursor cursor) {
439        if (cursor.moveToPosition(mPosition) && !isPhotoBound()) {
440            final LoaderManager manager = getLoaderManager();
441            final Loader<Bitmap> fakeLoader = manager.getLoader(LOADER_ID_PHOTO);
442            if (fakeLoader == null) {
443                return;
444            }
445
446            final PhotoBitmapLoader loader =
447                    (PhotoBitmapLoader) fakeLoader;
448            mResolvedPhotoUri = mAdapter.getPhotoUri(cursor);
449            loader.setPhotoUri(mResolvedPhotoUri);
450            loader.forceLoad();
451        }
452    }
453
454    public ProgressBarWrapper getPhotoProgressBar() {
455        return mPhotoProgressBar;
456    }
457
458    public TextView getEmptyText() {
459        return mEmptyText;
460    }
461
462    public ImageView getRetryButton() {
463        return mRetryButton;
464    }
465
466    public boolean isProgressBarNeeded() {
467        return mProgressBarNeeded;
468    }
469}
470