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.content.pm.PackageManager;
25import android.database.Cursor;
26import android.graphics.Bitmap;
27import android.net.ConnectivityManager;
28import android.net.NetworkInfo;
29import android.os.Bundle;
30import android.support.v4.app.Fragment;
31import android.support.v4.app.LoaderManager;
32import android.support.v4.content.Loader;
33import android.util.DisplayMetrics;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.View.OnClickListener;
37import android.view.ViewGroup;
38import android.view.WindowManager;
39import android.widget.ImageView;
40import android.widget.ProgressBar;
41import android.widget.TextView;
42
43import com.android.ex.photo.Intents;
44import com.android.ex.photo.PhotoViewCallbacks;
45import com.android.ex.photo.PhotoViewCallbacks.CursorChangedListener;
46import com.android.ex.photo.PhotoViewCallbacks.OnScreenListener;
47import com.android.ex.photo.R;
48import com.android.ex.photo.adapters.PhotoPagerAdapter;
49import com.android.ex.photo.loaders.PhotoBitmapLoader;
50import com.android.ex.photo.loaders.PhotoBitmapLoader.BitmapResult;
51import com.android.ex.photo.util.ImageUtils;
52import com.android.ex.photo.views.PhotoView;
53import com.android.ex.photo.views.ProgressBarWrapper;
54
55/**
56 * Displays a photo.
57 */
58public class PhotoViewFragment extends Fragment implements
59        LoaderManager.LoaderCallbacks<BitmapResult>,
60        OnClickListener,
61        OnScreenListener,
62        CursorChangedListener {
63    /**
64     * Interface for components that are internally scrollable left-to-right.
65     */
66    public static interface HorizontallyScrollable {
67        /**
68         * Return {@code true} if the component needs to receive right-to-left
69         * touch movements.
70         *
71         * @param origX the raw x coordinate of the initial touch
72         * @param origY the raw y coordinate of the initial touch
73         */
74
75        public boolean interceptMoveLeft(float origX, float origY);
76
77        /**
78         * Return {@code true} if the component needs to receive left-to-right
79         * touch movements.
80         *
81         * @param origX the raw x coordinate of the initial touch
82         * @param origY the raw y coordinate of the initial touch
83         */
84        public boolean interceptMoveRight(float origX, float origY);
85    }
86
87    protected final static String STATE_INTENT_KEY =
88            "com.android.mail.photo.fragments.PhotoViewFragment.INTENT";
89
90    private final static String ARG_INTENT = "arg-intent";
91    private final static String ARG_POSITION = "arg-position";
92    private final static String ARG_SHOW_SPINNER = "arg-show-spinner";
93
94    // Loader IDs
95    protected final static int LOADER_ID_PHOTO = 1;
96    protected final static int LOADER_ID_THUMBNAIL = 2;
97
98    /** The size of the photo */
99    public static Integer sPhotoSize;
100
101    /** The URL of a photo to display */
102    protected String mResolvedPhotoUri;
103    protected String mThumbnailUri;
104    /** The intent we were launched with */
105    protected Intent mIntent;
106    protected PhotoViewCallbacks mCallback;
107    protected PhotoPagerAdapter mAdapter;
108
109    protected PhotoView mPhotoView;
110    protected ImageView mPhotoPreviewImage;
111    protected TextView mEmptyText;
112    protected ImageView mRetryButton;
113    protected ProgressBarWrapper mPhotoProgressBar;
114
115    protected int mPosition;
116
117    /** Whether or not the fragment should make the photo full-screen */
118    protected boolean mFullScreen;
119
120    /** Whether or not this fragment will only show the loading spinner */
121    protected boolean mOnlyShowSpinner;
122
123    /** Whether or not the progress bar is showing valid information about the progress stated */
124    protected boolean mProgressBarNeeded = true;
125
126    protected View mPhotoPreviewAndProgress;
127    protected boolean mThumbnailShown;
128
129    /** Whether or not there is currently a connection to the internet */
130    protected boolean mConnected;
131
132    /** Public no-arg constructor for allowing the framework to handle orientation changes */
133    public PhotoViewFragment() {
134        // Do nothing.
135    }
136
137    /**
138     * Create a {@link PhotoViewFragment}.
139     * @param intent
140     * @param position
141     * @param onlyShowSpinner
142     */
143    public static PhotoViewFragment newInstance(
144            Intent intent, int position, boolean onlyShowSpinner) {
145        final Bundle b = new Bundle();
146        b.putParcelable(ARG_INTENT, intent);
147        b.putInt(ARG_POSITION, position);
148        b.putBoolean(ARG_SHOW_SPINNER, onlyShowSpinner);
149        final PhotoViewFragment f = new PhotoViewFragment();
150        f.setArguments(b);
151        return f;
152    }
153
154    @Override
155    public void onActivityCreated(Bundle savedInstanceState) {
156        super.onActivityCreated(savedInstanceState);
157        mCallback = (PhotoViewCallbacks) getActivity();
158        if (mCallback == null) {
159            throw new IllegalArgumentException(
160                    "Activity must be a derived class of PhotoViewActivity");
161        }
162        mAdapter = mCallback.getAdapter();
163        if (mAdapter == null) {
164            throw new IllegalStateException("Callback reported null adapter");
165        }
166
167        if (hasNetworkStatePermission()) {
168            getActivity().registerReceiver(new InternetStateBroadcastReceiver(),
169                    new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"));
170        }
171        // Don't call until we've setup the entire view
172        setViewVisibility();
173    }
174
175    @Override
176    public void onDetach() {
177        mCallback = null;
178        super.onDetach();
179    }
180
181    @Override
182    public void onCreate(Bundle savedInstanceState) {
183        super.onCreate(savedInstanceState);
184        if (sPhotoSize == null) {
185            final DisplayMetrics metrics = new DisplayMetrics();
186            final WindowManager wm =
187                    (WindowManager) getActivity().getSystemService(Context.WINDOW_SERVICE);
188            final ImageUtils.ImageSize imageSize = ImageUtils.sUseImageSize;
189            wm.getDefaultDisplay().getMetrics(metrics);
190            switch (imageSize) {
191                case EXTRA_SMALL:
192                    // Use a photo that's 80% of the "small" size
193                    sPhotoSize = (Math.min(metrics.heightPixels, metrics.widthPixels) * 800) / 1000;
194                    break;
195                case SMALL:
196                    // Fall through.
197                case NORMAL:
198                    // Fall through.
199                default:
200                    sPhotoSize = Math.min(metrics.heightPixels, metrics.widthPixels);
201                    break;
202            }
203        }
204
205        final Bundle bundle = getArguments();
206        if (bundle == null) {
207            return;
208        }
209        mIntent = bundle.getParcelable(ARG_INTENT);
210        mPosition = bundle.getInt(ARG_POSITION);
211        mOnlyShowSpinner = bundle.getBoolean(ARG_SHOW_SPINNER);
212        mProgressBarNeeded = true;
213
214        if (savedInstanceState != null) {
215            final Bundle state = savedInstanceState.getBundle(STATE_INTENT_KEY);
216            if (state != null) {
217                mIntent = new Intent().putExtras(state);
218            }
219        }
220
221        if (mIntent != null) {
222            mResolvedPhotoUri = mIntent.getStringExtra(Intents.EXTRA_RESOLVED_PHOTO_URI);
223            mThumbnailUri = mIntent.getStringExtra(Intents.EXTRA_THUMBNAIL_URI);
224        }
225    }
226
227    @Override
228    public View onCreateView(LayoutInflater inflater, ViewGroup container,
229            Bundle savedInstanceState) {
230        final View view = inflater.inflate(R.layout.photo_fragment_view, container, false);
231        initializeView(view);
232        return view;
233    }
234
235    protected void initializeView(View view) {
236        mPhotoView = (PhotoView) view.findViewById(R.id.photo_view);
237        mPhotoView.setMaxInitialScale(mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1));
238        mPhotoView.setOnClickListener(this);
239        mPhotoView.setFullScreen(mFullScreen, false);
240        mPhotoView.enableImageTransforms(false);
241
242        mPhotoPreviewAndProgress = view.findViewById(R.id.photo_preview);
243        mPhotoPreviewImage = (ImageView) view.findViewById(R.id.photo_preview_image);
244        mThumbnailShown = false;
245        final ProgressBar indeterminate =
246                (ProgressBar) view.findViewById(R.id.indeterminate_progress);
247        final ProgressBar determinate =
248                (ProgressBar) view.findViewById(R.id.determinate_progress);
249        mPhotoProgressBar = new ProgressBarWrapper(determinate, indeterminate, true);
250        mEmptyText = (TextView) view.findViewById(R.id.empty_text);
251        mRetryButton = (ImageView) view.findViewById(R.id.retry_button);
252    }
253
254    @Override
255    public void onResume() {
256        super.onResume();
257        mCallback.addScreenListener(mPosition, this);
258        mCallback.addCursorListener(this);
259
260        if (hasNetworkStatePermission()) {
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(LOADER_ID_THUMBNAIL, null, this);
278            getLoaderManager().initLoader(LOADER_ID_PHOTO, null, this);
279        }
280    }
281
282    @Override
283    public void onPause() {
284        // Remove listeners
285        mCallback.removeCursorListener(this);
286        mCallback.removeScreenListener(mPosition);
287        resetPhotoView();
288        super.onPause();
289    }
290
291    @Override
292    public void onDestroyView() {
293        // Clean up views and other components
294        if (mPhotoView != null) {
295            mPhotoView.clear();
296            mPhotoView = null;
297        }
298        super.onDestroyView();
299    }
300
301    @Override
302    public void onSaveInstanceState(Bundle outState) {
303        super.onSaveInstanceState(outState);
304
305        if (mIntent != null) {
306            outState.putParcelable(STATE_INTENT_KEY, mIntent.getExtras());
307        }
308    }
309
310    @Override
311    public Loader<BitmapResult> onCreateLoader(int id, Bundle args) {
312        if(mOnlyShowSpinner) {
313            return null;
314        }
315        switch (id) {
316            case LOADER_ID_PHOTO:
317                return new PhotoBitmapLoader(getActivity(), mResolvedPhotoUri);
318            case LOADER_ID_THUMBNAIL:
319                return new PhotoBitmapLoader(getActivity(), mThumbnailUri);
320            default:
321                return null;
322        }
323    }
324
325    @Override
326    public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) {
327        Bitmap data = result.bitmap;
328        // If we don't have a view, the fragment has been paused. We'll get the cursor again later.
329        if (getView() == null) {
330            return;
331        }
332
333        final int id = loader.getId();
334        switch (id) {
335            case LOADER_ID_THUMBNAIL:
336                if (isPhotoBound()) {
337                    // There is need to do anything with the thumbnail
338                    // image, as the full size image is being shown.
339                    return;
340                }
341
342                if (data == null) {
343                    // no preview, show default
344                    mPhotoPreviewImage.setImageResource(R.drawable.default_image);
345                    mThumbnailShown = false;
346                } else {
347                    // show preview
348                    mPhotoPreviewImage.setImageBitmap(data);
349                    mThumbnailShown = true;
350                }
351                mPhotoPreviewImage.setVisibility(View.VISIBLE);
352                if (getResources().getBoolean(R.bool.force_thumbnail_no_scaling)) {
353                    mPhotoPreviewImage.setScaleType(ImageView.ScaleType.CENTER);
354                }
355                enableImageTransforms(false);
356                break;
357            case LOADER_ID_PHOTO:
358
359                if (result.status == BitmapResult.STATUS_EXCEPTION) {
360                    mProgressBarNeeded = false;
361                    mEmptyText.setText(R.string.failed);
362                    mEmptyText.setVisibility(View.VISIBLE);
363                } else {
364                    bindPhoto(data);
365                }
366                break;
367            default:
368                break;
369        }
370
371        if (mProgressBarNeeded == false) {
372            // Hide the progress bar as it isn't needed anymore.
373            mPhotoProgressBar.setVisibility(View.GONE);
374        }
375
376        if (data != null) {
377            mCallback.onNewPhotoLoaded(mPosition);
378        }
379        setViewVisibility();
380    }
381
382    /**
383     * Binds an image to the photo view.
384     */
385    private void bindPhoto(Bitmap bitmap) {
386        if (bitmap != null) {
387            if (mPhotoView != null) {
388                mPhotoView.bindPhoto(bitmap);
389            }
390            enableImageTransforms(true);
391            mPhotoPreviewAndProgress.setVisibility(View.GONE);
392            mProgressBarNeeded = false;
393        }
394    }
395
396    private boolean hasNetworkStatePermission() {
397        final String networkStatePermission = "android.permission.ACCESS_NETWORK_STATE";
398        int result = getActivity().checkCallingOrSelfPermission(networkStatePermission);
399        return result == PackageManager.PERMISSION_GRANTED;
400    }
401
402    /**
403     * Enable or disable image transformations. When transformations are enabled, this view
404     * consumes all touch events.
405     */
406    public void enableImageTransforms(boolean enable) {
407        mPhotoView.enableImageTransforms(enable);
408    }
409
410    /**
411     * Resets the photo view to it's default state w/ no bound photo.
412     */
413    private void resetPhotoView() {
414        if (mPhotoView != null) {
415            mPhotoView.bindPhoto(null);
416        }
417    }
418
419    @Override
420    public void onLoaderReset(Loader<BitmapResult> loader) {
421        // Do nothing
422    }
423
424    @Override
425    public void onClick(View v) {
426        mCallback.toggleFullScreen();
427    }
428
429    @Override
430    public void onFullScreenChanged(boolean fullScreen) {
431        setViewVisibility();
432    }
433
434    @Override
435    public void onViewActivated() {
436        if (!mCallback.isFragmentActive(this)) {
437            // we're not in the foreground; reset our view
438            resetViews();
439        } else {
440            if (!isPhotoBound()) {
441                // Restart the loader
442                getLoaderManager().restartLoader(LOADER_ID_THUMBNAIL, null, this);
443            }
444            mCallback.onFragmentVisible(this);
445        }
446    }
447
448    /**
449     * Reset the views to their default states
450     */
451    public void resetViews() {
452        if (mPhotoView != null) {
453            mPhotoView.resetTransformations();
454        }
455    }
456
457    @Override
458    public boolean onInterceptMoveLeft(float origX, float origY) {
459        if (!mCallback.isFragmentActive(this)) {
460            // we're not in the foreground; don't intercept any touches
461            return false;
462        }
463
464        return (mPhotoView != null && mPhotoView.interceptMoveLeft(origX, origY));
465    }
466
467    @Override
468    public boolean onInterceptMoveRight(float origX, float origY) {
469        if (!mCallback.isFragmentActive(this)) {
470            // we're not in the foreground; don't intercept any touches
471            return false;
472        }
473
474        return (mPhotoView != null && mPhotoView.interceptMoveRight(origX, origY));
475    }
476
477    /**
478     * Returns {@code true} if a photo has been bound. Otherwise, returns {@code false}.
479     */
480    public boolean isPhotoBound() {
481        return (mPhotoView != null && mPhotoView.isPhotoBound());
482    }
483
484    /**
485     * Sets view visibility depending upon whether or not we're in "full screen" mode.
486     */
487    private void setViewVisibility() {
488        final boolean fullScreen = mCallback == null ? false : mCallback.isFragmentFullScreen(this);
489        setFullScreen(fullScreen);
490    }
491
492    /**
493     * Sets full-screen mode for the views.
494     */
495    public void setFullScreen(boolean fullScreen) {
496        mFullScreen = fullScreen;
497    }
498
499    @Override
500    public void onCursorChanged(Cursor cursor) {
501        if (mAdapter == null) {
502            // The adapter is set in onAttach(), and is guaranteed to be non-null. We have magically
503            // received an onCursorChanged without attaching to an activity. Ignore this cursor
504            // change.
505            return;
506        }
507        if (cursor.moveToPosition(mPosition) && !isPhotoBound()) {
508            mCallback.onCursorChanged(this, cursor);
509
510            final LoaderManager manager = getLoaderManager();
511            final Loader<BitmapResult> fakePhotoLoader = manager.getLoader(LOADER_ID_PHOTO);
512            if (fakePhotoLoader != null) {
513                final PhotoBitmapLoader loader = (PhotoBitmapLoader) fakePhotoLoader;
514                mResolvedPhotoUri = mAdapter.getPhotoUri(cursor);
515                loader.setPhotoUri(mResolvedPhotoUri);
516                loader.forceLoad();
517            }
518
519            if (!mThumbnailShown) {
520                final Loader<BitmapResult> fakeThumbnailLoader = manager.getLoader(
521                        LOADER_ID_THUMBNAIL);
522                if (fakeThumbnailLoader != null) {
523                    final PhotoBitmapLoader loader = (PhotoBitmapLoader) fakeThumbnailLoader;
524                    mThumbnailUri = mAdapter.getThumbnailUri(cursor);
525                    loader.setPhotoUri(mThumbnailUri);
526                    loader.forceLoad();
527                }
528            }
529        }
530    }
531
532    public ProgressBarWrapper getPhotoProgressBar() {
533        return mPhotoProgressBar;
534    }
535
536    public TextView getEmptyText() {
537        return mEmptyText;
538    }
539
540    public ImageView getRetryButton() {
541        return mRetryButton;
542    }
543
544    public boolean isProgressBarNeeded() {
545        return mProgressBarNeeded;
546    }
547
548    private class InternetStateBroadcastReceiver extends BroadcastReceiver {
549
550        @Override
551        public void onReceive(Context context, Intent intent) {
552            // This is only created if we have the correct permissions, so
553            ConnectivityManager connectivityManager = (ConnectivityManager)
554                    context.getSystemService(Context.CONNECTIVITY_SERVICE);
555            NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
556            if (activeNetInfo == null) {
557                mConnected = false;
558                return;
559            }
560            if (mConnected == false && activeNetInfo.isConnected() && !isPhotoBound()) {
561                if (mThumbnailShown == false) {
562                    getLoaderManager().restartLoader(LOADER_ID_THUMBNAIL, null,
563                            PhotoViewFragment.this);
564                }
565                getLoaderManager().restartLoader(LOADER_ID_PHOTO, null, PhotoViewFragment.this);
566                mConnected = true;
567                mPhotoProgressBar.setVisibility(View.VISIBLE);
568            }
569        }
570    }
571}
572