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