PhotoViewActivity.java revision 4f49ea6da326194dffa0348a83d8110ac30d92e4
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;
19
20import android.app.Activity;
21import android.app.ActivityManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.res.Resources;
25import android.database.Cursor;
26import android.graphics.Bitmap;
27import android.graphics.drawable.BitmapDrawable;
28import android.graphics.drawable.Drawable;
29import android.net.Uri;
30import android.os.Build;
31import android.os.Bundle;
32import android.os.Handler;
33import android.support.v4.app.Fragment;
34import android.support.v4.app.LoaderManager;
35import android.support.v4.content.Loader;
36import android.support.v4.view.ViewPager.OnPageChangeListener;
37import android.view.MenuItem;
38import android.support.v7.app.ActionBar;
39import android.support.v7.app.ActionBar.OnMenuVisibilityListener;
40import android.support.v7.app.ActionBarActivity;
41import android.text.TextUtils;
42import android.util.Log;
43import android.view.View;
44import android.view.ViewPropertyAnimator;
45import android.view.ViewTreeObserver.OnGlobalLayoutListener;
46import android.view.animation.AlphaAnimation;
47import android.view.animation.Animation;
48import android.view.animation.AnimationSet;
49import android.view.animation.ScaleAnimation;
50import android.view.animation.TranslateAnimation;
51import android.view.animation.Animation.AnimationListener;
52import android.widget.ImageView;
53
54import com.android.ex.photo.PhotoViewPager.InterceptType;
55import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener;
56import com.android.ex.photo.adapters.PhotoPagerAdapter;
57import com.android.ex.photo.fragments.PhotoViewFragment;
58import com.android.ex.photo.loaders.PhotoBitmapLoader;
59import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
60import com.android.ex.photo.loaders.PhotoPagerLoader;
61import com.android.ex.photo.provider.PhotoContract;
62
63import java.util.HashMap;
64import java.util.HashSet;
65import java.util.Map;
66import java.util.Set;
67
68/**
69 * Activity to view the contents of an album.
70 */
71public class PhotoViewActivity extends ActionBarActivity implements
72        LoaderManager.LoaderCallbacks<Cursor>, OnPageChangeListener, OnInterceptTouchListener,
73        OnMenuVisibilityListener, PhotoViewCallbacks {
74
75    private final static String TAG = "PhotoViewActivity";
76
77    private final static String STATE_CURRENT_URI_KEY =
78            "com.google.android.apps.plus.PhotoViewFragment.CURRENT_URI";
79    private final static String STATE_CURRENT_INDEX_KEY =
80            "com.google.android.apps.plus.PhotoViewFragment.CURRENT_INDEX";
81    private final static String STATE_FULLSCREEN_KEY =
82            "com.google.android.apps.plus.PhotoViewFragment.FULLSCREEN";
83    private final static String STATE_ACTIONBARTITLE_KEY =
84            "com.google.android.apps.plus.PhotoViewFragment.ACTIONBARTITLE";
85    private final static String STATE_ACTIONBARSUBTITLE_KEY =
86            "com.google.android.apps.plus.PhotoViewFragment.ACTIONBARTITLE";
87    private final static String STATE_ENTERANIMATIONFINISHED_KEY =
88            "com.google.android.apps.plus.PhotoViewFragment.SCALEANIMATIONFINISHED";
89
90    protected final static String ARG_IMAGE_URI = "image_uri";
91
92    private static final int LOADER_PHOTO_LIST = 100;
93
94    /** Count used when the real photo count is unknown [but, may be determined] */
95    public static final int ALBUM_COUNT_UNKNOWN = -1;
96
97    public static final int ENTER_ANIMATION_DURATION_MS = 250;
98    public static final int EXIT_ANIMATION_DURATION_MS = 250;
99
100    /** Argument key for the dialog message */
101    public static final String KEY_MESSAGE = "dialog_message";
102
103    public static int sMemoryClass;
104
105    /** The URI of the photos we're viewing; may be {@code null} */
106    private String mPhotosUri;
107    /** The index of the currently viewed photo */
108    private int mCurrentPhotoIndex;
109    /** The uri of the currently viewed photo */
110    private String mCurrentPhotoUri;
111    /** The query projection to use; may be {@code null} */
112    private String[] mProjection;
113    /** The total number of photos; only valid if {@link #mIsEmpty} is {@code false}. */
114    protected int mAlbumCount = ALBUM_COUNT_UNKNOWN;
115    /** {@code true} if the view is empty. Otherwise, {@code false}. */
116    protected boolean mIsEmpty;
117    /** the main root view */
118    protected View mRootView;
119    /** Background image that contains nothing, so it can be alpha faded from
120     * transparent to black without affecting any other views. */
121    protected View mBackground;
122    /** The main pager; provides left/right swipe between photos */
123    protected PhotoViewPager mViewPager;
124    /** The temporary image so that we can quickly scale up the fullscreen thumbnail */
125    protected ImageView mTemporaryImage;
126    /** Adapter to create pager views */
127    protected PhotoPagerAdapter mAdapter;
128    /** Whether or not we're in "full screen" mode */
129    protected boolean mFullScreen;
130    /** The listeners wanting full screen state for each screen position */
131    private final Map<Integer, OnScreenListener>
132            mScreenListeners = new HashMap<Integer, OnScreenListener>();
133    /** The set of listeners wanting full screen state */
134    private final Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>();
135    /** When {@code true}, restart the loader when the activity becomes active */
136    private boolean mRestartLoader;
137    /** Whether or not this activity is paused */
138    protected boolean mIsPaused = true;
139    /** The maximum scale factor applied to images when they are initially displayed */
140    protected float mMaxInitialScale;
141    /** The title in the actionbar */
142    protected String mActionBarTitle;
143    /** The subtitle in the actionbar */
144    protected String mActionBarSubtitle;
145
146    private boolean mEnterAnimationFinished;
147    protected boolean mScaleAnimationEnabled;
148    protected int mAnimationStartX;
149    protected int mAnimationStartY;
150    protected int mAnimationStartWidth;
151    protected int mAnimationStartHeight;
152
153    protected boolean mActionBarHiddenInitially;
154    protected boolean mDisplayThumbsFullScreen;
155
156    protected BitmapCallback mBitmapCallback;
157    protected final Handler mHandler = new Handler();
158
159    // TODO Find a better way to do this. We basically want the activity to display the
160    // "loading..." progress until the fragment takes over and shows it's own "loading..."
161    // progress [located in photo_header_view.xml]. We could potentially have all status displayed
162    // by the activity, but, that gets tricky when it comes to screen rotation. For now, we
163    // track the loading by this variable which is fragile and may cause phantom "loading..."
164    // text.
165    private long mEnterFullScreenDelayTime;
166
167
168    protected PhotoPagerAdapter createPhotoPagerAdapter(Context context,
169            android.support.v4.app.FragmentManager fm, Cursor c, float maxScale) {
170        PhotoPagerAdapter adapter = new PhotoPagerAdapter(context, fm, c, maxScale,
171                mDisplayThumbsFullScreen);
172        return adapter;
173    }
174
175    @Override
176    protected void onCreate(Bundle savedInstanceState) {
177        super.onCreate(savedInstanceState);
178
179        final ActivityManager mgr = (ActivityManager) getApplicationContext().
180                getSystemService(Activity.ACTIVITY_SERVICE);
181        sMemoryClass = mgr.getMemoryClass();
182
183        final Intent intent = getIntent();
184        // uri of the photos to view; optional
185        if (intent.hasExtra(Intents.EXTRA_PHOTOS_URI)) {
186            mPhotosUri = intent.getStringExtra(Intents.EXTRA_PHOTOS_URI);
187        }
188        if (intent.getBooleanExtra(Intents.EXTRA_SCALE_UP_ANIMATION, false)) {
189            mScaleAnimationEnabled = true;
190            mAnimationStartX = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_X, 0);
191            mAnimationStartY = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_Y, 0);
192            mAnimationStartWidth = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_WIDTH, 0);
193            mAnimationStartHeight = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_HEIGHT, 0);
194        }
195        mActionBarHiddenInitially = intent.getBooleanExtra(
196                Intents.EXTRA_ACTION_BAR_HIDDEN_INITIALLY, false);
197        mDisplayThumbsFullScreen = intent.getBooleanExtra(
198                Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false);
199
200        // projection for the query; optional
201        // If not set, the default projection is used.
202        // This projection must include the columns from the default projection.
203        if (intent.hasExtra(Intents.EXTRA_PROJECTION)) {
204            mProjection = intent.getStringArrayExtra(Intents.EXTRA_PROJECTION);
205        } else {
206            mProjection = null;
207        }
208
209        // Set the max initial scale, defaulting to 1x
210        mMaxInitialScale = intent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f);
211        mCurrentPhotoUri = null;
212        mCurrentPhotoIndex = -1;
213
214        // We allow specifying the current photo by either index or uri.
215        // This is because some users may have live datasets that can change,
216        // adding new items to either the beginning or end of the set. For clients
217        // that do not need that capability, ability to specify the current photo
218        // by index is offered as a convenience.
219        if (intent.hasExtra(Intents.EXTRA_PHOTO_INDEX)) {
220            mCurrentPhotoIndex = intent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1);
221        }
222        if (intent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI)) {
223            mCurrentPhotoUri = intent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI);
224        }
225        mIsEmpty = true;
226
227        if (savedInstanceState != null) {
228            mCurrentPhotoUri = savedInstanceState.getString(STATE_CURRENT_URI_KEY);
229            mCurrentPhotoIndex = savedInstanceState.getInt(STATE_CURRENT_INDEX_KEY);
230            mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false);
231            mActionBarTitle = savedInstanceState.getString(STATE_ACTIONBARTITLE_KEY);
232            mActionBarSubtitle = savedInstanceState.getString(STATE_ACTIONBARSUBTITLE_KEY);
233            mEnterAnimationFinished = savedInstanceState.getBoolean(
234                    STATE_ENTERANIMATIONFINISHED_KEY, false);
235        } else {
236            mFullScreen = mActionBarHiddenInitially;
237        }
238
239        setContentView(R.layout.photo_activity_view);
240
241        // Create the adapter and add the view pager
242        mAdapter =
243                createPhotoPagerAdapter(this, getSupportFragmentManager(), null, mMaxInitialScale);
244        final Resources resources = getResources();
245        mRootView = findViewById(R.id.photo_activity_root_view);
246        mBackground = findViewById(R.id.photo_activity_background);
247        mTemporaryImage = (ImageView) findViewById(R.id.photo_activity_temporary_image);
248        mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager);
249        mViewPager.setAdapter(mAdapter);
250        mViewPager.setOnPageChangeListener(this);
251        mViewPager.setOnInterceptTouchListener(this);
252        mViewPager.setPageMargin(resources.getDimensionPixelSize(R.dimen.photo_page_margin));
253
254        mBitmapCallback = new BitmapCallback();
255        if (!mScaleAnimationEnabled || mEnterAnimationFinished) {
256            // We are not running the scale up animation. Just let the fragments
257            // display and handle the animation.
258            getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
259            // Make the background opaque immediately so that we don't see the activity
260            // behind this one.
261            mBackground.setVisibility(View.VISIBLE);
262        } else {
263            // Attempt to load the initial image thumbnail. Once we have the
264            // image, animate it up. Once the animation is complete, we can kick off
265            // loading the ViewPager. After the primary fullres image is loaded, we will
266            // make our temporary image invisible and display the ViewPager.
267            mViewPager.setVisibility(View.GONE);
268            Bundle args = new Bundle();
269            args.putString(ARG_IMAGE_URI, mCurrentPhotoUri);
270            getSupportLoaderManager().initLoader(BITMAP_LOADER_THUMBNAIL, args, mBitmapCallback);
271        }
272
273        mEnterFullScreenDelayTime =
274                resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis);
275
276        final ActionBar actionBar = getSupportActionBar();
277        if (actionBar != null) {
278            actionBar.setDisplayHomeAsUpEnabled(true);
279            actionBar.addOnMenuVisibilityListener(this);
280            final int showTitle = ActionBar.DISPLAY_SHOW_TITLE;
281            actionBar.setDisplayOptions(showTitle, showTitle);
282            // Set the title and subtitle immediately here, rather than waiting
283            // for the fragment to be initialized.
284            setActionBarTitles(actionBar);
285        }
286
287        setLightsOutMode(mFullScreen);
288    }
289
290    @Override
291    protected void onResume() {
292        super.onResume();
293        setFullScreen(mFullScreen, false);
294
295        mIsPaused = false;
296        if (mRestartLoader) {
297            mRestartLoader = false;
298            getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this);
299        }
300    }
301
302    @Override
303    protected void onPause() {
304        mIsPaused = true;
305        super.onPause();
306    }
307
308    @Override
309    public void onBackPressed() {
310        // If we are in fullscreen mode, and the default is not full screen, then
311        // switch back to actionBar display mode.
312        if (mFullScreen && !mActionBarHiddenInitially) {
313            toggleFullScreen();
314        } else {
315            if (mScaleAnimationEnabled) {
316                runExitAnimation();
317            } else {
318                super.onBackPressed();
319            }
320        }
321    }
322
323    @Override
324    public void onSaveInstanceState(Bundle outState) {
325        super.onSaveInstanceState(outState);
326
327        outState.putString(STATE_CURRENT_URI_KEY, mCurrentPhotoUri);
328        outState.putInt(STATE_CURRENT_INDEX_KEY, mCurrentPhotoIndex);
329        outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen);
330        outState.putString(STATE_ACTIONBARTITLE_KEY, mActionBarTitle);
331        outState.putString(STATE_ACTIONBARSUBTITLE_KEY, mActionBarSubtitle);
332        outState.putBoolean(STATE_ENTERANIMATIONFINISHED_KEY, mEnterAnimationFinished);
333    }
334
335    @Override
336    public boolean onOptionsItemSelected(MenuItem item) {
337       switch (item.getItemId()) {
338          case android.R.id.home:
339             finish();
340             return true;
341          default:
342             return super.onOptionsItemSelected(item);
343       }
344    }
345
346    @Override
347    public void addScreenListener(int position, OnScreenListener listener) {
348        mScreenListeners.put(position, listener);
349    }
350
351    @Override
352    public void removeScreenListener(int position) {
353        mScreenListeners.remove(position);
354    }
355
356    @Override
357    public synchronized void addCursorListener(CursorChangedListener listener) {
358        mCursorListeners.add(listener);
359    }
360
361    @Override
362    public synchronized void removeCursorListener(CursorChangedListener listener) {
363        mCursorListeners.remove(listener);
364    }
365
366    @Override
367    public boolean isFragmentFullScreen(Fragment fragment) {
368        if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) {
369            return mFullScreen;
370        }
371        return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment));
372    }
373
374    @Override
375    public void toggleFullScreen() {
376        setFullScreen(!mFullScreen, true);
377    }
378
379    public void onPhotoRemoved(long photoId) {
380        final Cursor data = mAdapter.getCursor();
381        if (data == null) {
382            // Huh?! How would this happen?
383            return;
384        }
385
386        final int dataCount = data.getCount();
387        if (dataCount <= 1) {
388            finish();
389            return;
390        }
391
392        getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this);
393    }
394
395    @Override
396    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
397        if (id == LOADER_PHOTO_LIST) {
398            return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mProjection);
399        }
400        return null;
401    }
402
403    @Override
404    public Loader<BitmapResult> onCreateBitmapLoader(int id, Bundle args, String uri) {
405        switch (id) {
406            case BITMAP_LOADER_AVATAR:
407            case BITMAP_LOADER_THUMBNAIL:
408            case BITMAP_LOADER_PHOTO:
409                return new PhotoBitmapLoader(this, uri);
410            default:
411                return null;
412        }
413    }
414
415    @Override
416    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
417
418        final int id = loader.getId();
419        if (id == LOADER_PHOTO_LIST) {
420            if (data == null || data.getCount() == 0) {
421                mIsEmpty = true;
422            } else {
423                mAlbumCount = data.getCount();
424                if (mCurrentPhotoUri != null) {
425                    int index = 0;
426                    // Clear query params. Compare only the path.
427                    final int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
428                    final Uri currentPhotoUri;
429                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
430                        currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon()
431                            .clearQuery().build();
432                    } else {
433                        currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon()
434                            .query(null).build();
435                    }
436                    while (data.moveToNext()) {
437                        final String uriString = data.getString(uriIndex);
438                        final Uri uri;
439                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
440                            uri = Uri.parse(uriString).buildUpon().clearQuery().build();
441                        } else {
442                            uri = Uri.parse(uriString).buildUpon().query(null).build();
443                        }
444                        if (currentPhotoUri != null && currentPhotoUri.equals(uri)) {
445                            mCurrentPhotoIndex = index;
446                            break;
447                        }
448                        index++;
449                    }
450                }
451
452                // We're paused; don't do anything now, we'll get re-invoked
453                // when the activity becomes active again
454                // TODO(pwestbro): This shouldn't be necessary, as the loader manager should
455                // restart the loader
456                if (mIsPaused) {
457                    mRestartLoader = true;
458                    return;
459                }
460                boolean wasEmpty = mIsEmpty;
461                mIsEmpty = false;
462
463                mAdapter.swapCursor(data);
464                if (mViewPager.getAdapter() == null) {
465                    mViewPager.setAdapter(mAdapter);
466                }
467                notifyCursorListeners(data);
468
469                // Use an index of 0 if the index wasn't specified or couldn't be found
470                if (mCurrentPhotoIndex < 0) {
471                    mCurrentPhotoIndex = 0;
472                }
473
474                mViewPager.setCurrentItem(mCurrentPhotoIndex, false);
475                if (wasEmpty) {
476                    setViewActivated(mCurrentPhotoIndex);
477                }
478            }
479            // Update the any action items
480            updateActionItems();
481        }
482    }
483
484    @Override
485    public void onLoaderReset(android.support.v4.content.Loader<Cursor> loader) {
486        // If the loader is reset, remove the reference in the adapter to this cursor
487        // TODO(pwestbro): reenable this when b/7075236 is fixed
488        // mAdapter.swapCursor(null);
489    }
490
491    protected void updateActionItems() {
492        // Do nothing, but allow extending classes to do work
493    }
494
495    private synchronized void notifyCursorListeners(Cursor data) {
496        // tell all of the objects listening for cursor changes
497        // that the cursor has changed
498        for (CursorChangedListener listener : mCursorListeners) {
499            listener.onCursorChanged(data);
500        }
501    }
502
503    @Override
504    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
505    }
506
507    @Override
508    public void onPageSelected(int position) {
509        mCurrentPhotoIndex = position;
510        setViewActivated(position);
511    }
512
513    @Override
514    public void onPageScrollStateChanged(int state) {
515    }
516
517    @Override
518    public boolean isFragmentActive(Fragment fragment) {
519        if (mViewPager == null || mAdapter == null) {
520            return false;
521        }
522        return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment);
523    }
524
525    @Override
526    public void onFragmentVisible(PhotoViewFragment fragment) {
527        // Do nothing, we handle this in setViewActivated
528    }
529
530    @Override
531    public InterceptType onTouchIntercept(float origX, float origY) {
532        boolean interceptLeft = false;
533        boolean interceptRight = false;
534
535        for (OnScreenListener listener : mScreenListeners.values()) {
536            if (!interceptLeft) {
537                interceptLeft = listener.onInterceptMoveLeft(origX, origY);
538            }
539            if (!interceptRight) {
540                interceptRight = listener.onInterceptMoveRight(origX, origY);
541            }
542        }
543
544        if (interceptLeft) {
545            if (interceptRight) {
546                return InterceptType.BOTH;
547            }
548            return InterceptType.LEFT;
549        } else if (interceptRight) {
550            return InterceptType.RIGHT;
551        }
552        return InterceptType.NONE;
553    }
554
555    /**
556     * Updates the title bar according to the value of {@link #mFullScreen}.
557     */
558    protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) {
559        final boolean fullScreenChanged = (fullScreen != mFullScreen);
560        mFullScreen = fullScreen;
561
562        if (mFullScreen) {
563            setLightsOutMode(true);
564            cancelEnterFullScreenRunnable();
565        } else {
566            setLightsOutMode(false);
567            if (setDelayedRunnable) {
568                postEnterFullScreenRunnableWithDelay();
569            }
570        }
571
572        if (fullScreenChanged) {
573            for (OnScreenListener listener : mScreenListeners.values()) {
574                listener.onFullScreenChanged(mFullScreen);
575            }
576        }
577    }
578
579    private void postEnterFullScreenRunnableWithDelay() {
580        mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime);
581    }
582
583    private void cancelEnterFullScreenRunnable() {
584        mHandler.removeCallbacks(mEnterFullScreenRunnable);
585    }
586
587    protected void setLightsOutMode(boolean enabled) {
588        int flags = 0;
589        final int version = Build.VERSION.SDK_INT;
590        final ActionBar actionBar = getSupportActionBar();
591        if (enabled) {
592            if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
593                flags = View.SYSTEM_UI_FLAG_LOW_PROFILE
594                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
595                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
596                if (!mScaleAnimationEnabled) {
597                    // If we are using the scale animation for intro and exit,
598                    // we can't go into fullscreen mode. The issue is that the
599                    // activity that invoked this will not be in fullscreen, so
600                    // as we transition out, the background activity will be
601                    // temporarily rendered without an actionbar, and the shrinking
602                    // photo will not line up properly. After that it redraws
603                    // in the correct location, but it still looks janks.
604                    // FLAG: there may be a better way to fix this, but I don't
605                    // yet know what it is.
606                    flags |= View.SYSTEM_UI_FLAG_FULLSCREEN;
607                }
608            } else if (version >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
609                flags = View.SYSTEM_UI_FLAG_LOW_PROFILE;
610            } else if (version >= android.os.Build.VERSION_CODES.HONEYCOMB) {
611                flags = View.STATUS_BAR_HIDDEN;
612            }
613            actionBar.hide();
614        } else {
615            if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
616                flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
617                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
618            } else if (version >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
619                flags = View.SYSTEM_UI_FLAG_VISIBLE;
620            } else if (version >= android.os.Build.VERSION_CODES.HONEYCOMB) {
621                flags = View.STATUS_BAR_VISIBLE;
622            }
623            actionBar.show();
624        }
625
626        if (version >= Build.VERSION_CODES.HONEYCOMB) {
627            mRootView.setSystemUiVisibility(flags);
628        }
629    }
630
631    private final Runnable mEnterFullScreenRunnable = new Runnable() {
632        @Override
633        public void run() {
634            setFullScreen(true, true);
635        }
636    };
637
638    @Override
639    public void setViewActivated(int position) {
640        OnScreenListener listener = mScreenListeners.get(position);
641        if (listener != null) {
642            listener.onViewActivated();
643        }
644        final Cursor cursor = getCursorAtProperPosition();
645        mCurrentPhotoIndex = position;
646        // FLAG: get the column indexes once in onLoadFinished().
647        // That would make this more efficient, instead of looking these up
648        // repeatedly whenever we want them.
649        int uriIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
650        mCurrentPhotoUri = cursor.getString(uriIndex);
651        updateActionBar();
652
653        // Restart the timer to return to fullscreen.
654        cancelEnterFullScreenRunnable();
655        postEnterFullScreenRunnableWithDelay();
656    }
657
658    /**
659     * Adjusts the activity title and subtitle to reflect the photo name and count.
660     */
661    protected void updateActionBar() {
662        final int position = mViewPager.getCurrentItem() + 1;
663        final boolean hasAlbumCount = mAlbumCount >= 0;
664
665        final Cursor cursor = getCursorAtProperPosition();
666        if (cursor != null) {
667            // FLAG: We should grab the indexes when we first get the cursor
668            // and store them so we don't need to do it each time.
669            final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME);
670            mActionBarTitle = cursor.getString(photoNameIndex);
671        } else {
672            mActionBarTitle = null;
673        }
674
675        if (mIsEmpty || !hasAlbumCount || position <= 0) {
676            mActionBarSubtitle = null;
677        } else {
678            mActionBarSubtitle =
679                    getResources().getString(R.string.photo_view_count, position, mAlbumCount);
680        }
681
682        setActionBarTitles(getSupportActionBar());
683    }
684
685    /**
686     * Sets the Action Bar title to {@link #mActionBarTitle} and the subtitle to
687     * {@link #mActionBarSubtitle}
688     */
689    protected final void setActionBarTitles(ActionBar actionBar) {
690        if (actionBar == null) {
691            return;
692        }
693        actionBar.setTitle(getInputOrEmpty(mActionBarTitle));
694        actionBar.setSubtitle(getInputOrEmpty(mActionBarSubtitle));
695    }
696
697    /**
698     * If the input string is non-null, it is returned, otherwise an empty string is returned;
699     * @param in
700     * @return
701     */
702    private static final String getInputOrEmpty(String in) {
703        if (in == null) {
704            return "";
705        }
706        return in;
707    }
708
709    /**
710     * Utility method that will return the cursor that contains the data
711     * at the current position so that it refers to the current image on screen.
712     * @return the cursor at the current position or
713     * null if no cursor exists or if the {@link PhotoViewPager} is null.
714     */
715    public Cursor getCursorAtProperPosition() {
716        if (mViewPager == null) {
717            return null;
718        }
719
720        final int position = mViewPager.getCurrentItem();
721        final Cursor cursor = mAdapter.getCursor();
722
723        if (cursor == null) {
724            return null;
725        }
726
727        cursor.moveToPosition(position);
728
729        return cursor;
730    }
731
732    public Cursor getCursor() {
733        return (mAdapter == null) ? null : mAdapter.getCursor();
734    }
735
736    @Override
737    public void onMenuVisibilityChanged(boolean isVisible) {
738        if (isVisible) {
739            cancelEnterFullScreenRunnable();
740        } else {
741            postEnterFullScreenRunnableWithDelay();
742        }
743    }
744
745    @Override
746    public void onNewPhotoLoaded(int position) {
747        // do nothing
748    }
749
750    protected void setPhotoIndex(int index) {
751        mCurrentPhotoIndex = index;
752    }
753
754    @Override
755    public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success) {
756        if (mTemporaryImage.getVisibility() != View.GONE &&
757                TextUtils.equals(fragment.getPhotoUri(), mCurrentPhotoUri)) {
758            if (success) {
759                // The fragment for the current image is now ready for display.
760                mTemporaryImage.setVisibility(View.GONE);
761                mViewPager.setVisibility(View.VISIBLE);
762            } else {
763                // This means that we are unable to load the fragment's photo.
764                // I'm not sure what the best thing to do here is, but at least if
765                // we display the viewPager, the fragment itself can decide how to
766                // display the failure of its own image.
767                Log.w(TAG, "Failed to load fragment image");
768                mTemporaryImage.setVisibility(View.GONE);
769                mViewPager.setVisibility(View.VISIBLE);
770            }
771        }
772    }
773
774    protected boolean isFullScreen() {
775        return mFullScreen;
776    }
777
778    @Override
779    public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) {
780        // do nothing
781    }
782
783    @Override
784    public PhotoPagerAdapter getAdapter() {
785        return mAdapter;
786    }
787
788    public void onEnterAnimationComplete() {
789        mEnterAnimationFinished = true;
790        mViewPager.setVisibility(View.VISIBLE);
791    }
792
793    private void onExitAnimationComplete() {
794        finish();
795        overridePendingTransition(0, 0);
796    }
797
798    private void runEnterAnimation() {
799        final int totalWidth = mRootView.getMeasuredWidth();
800        final int totalHeight = mRootView.getMeasuredHeight();
801
802        // FLAG: Need to handle the aspect ratio of the bitmap.  If it's a portrait
803        // bitmap, then we need to position the view higher so that the middle
804        // pixels line up.
805        mTemporaryImage.setVisibility(View.VISIBLE);
806        // We need to take a full screen image, and scale/translate it so that
807        // it appears at exactly the same location onscreen as it is in the
808        // prior activity.
809        // The final image will take either the full screen width or height (or both).
810
811        final float scaleW = (float) mAnimationStartWidth / totalWidth;
812        final float scaleY = (float) mAnimationStartHeight / totalHeight;
813        final float scale = Math.max(scaleW, scaleY);
814
815        final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth,
816                totalWidth, scale);
817        final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight,
818                totalHeight, scale);
819
820        final int version = android.os.Build.VERSION.SDK_INT;
821        if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
822            mBackground.setAlpha(0f);
823            mBackground.animate().alpha(1f).setDuration(ENTER_ANIMATION_DURATION_MS).start();
824            mBackground.setVisibility(View.VISIBLE);
825
826            mTemporaryImage.setScaleX(scale);
827            mTemporaryImage.setScaleY(scale);
828            mTemporaryImage.setTranslationX(translateX);
829            mTemporaryImage.setTranslationY(translateY);
830
831            Runnable endRunnable = new Runnable() {
832                @Override
833                public void run() {
834                    PhotoViewActivity.this.onEnterAnimationComplete();
835                }
836            };
837            ViewPropertyAnimator animator = mTemporaryImage.animate().scaleX(1f).scaleY(1f)
838                .translationX(0).translationY(0).setDuration(ENTER_ANIMATION_DURATION_MS);
839            if (version >= Build.VERSION_CODES.JELLY_BEAN) {
840                animator.withEndAction(endRunnable);
841            } else {
842                mHandler.postDelayed(endRunnable, ENTER_ANIMATION_DURATION_MS);
843            }
844            animator.start();
845        } else {
846            final Animation alphaAnimation = new AlphaAnimation(0f, 1f);
847            alphaAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);
848            mBackground.startAnimation(alphaAnimation);
849            mBackground.setVisibility(View.VISIBLE);
850
851            final Animation translateAnimation = new TranslateAnimation(translateX,
852                    translateY, 0, 0);
853            translateAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);
854            Animation scaleAnimation = new ScaleAnimation(scale, scale, 0, 0);
855            scaleAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);
856
857            AnimationSet animationSet = new AnimationSet(true);
858            animationSet.addAnimation(translateAnimation);
859            animationSet.addAnimation(scaleAnimation);
860            AnimationListener listener = new AnimationListener() {
861                @Override
862                public void onAnimationEnd(Animation arg0) {
863                    PhotoViewActivity.this.onEnterAnimationComplete();
864                }
865
866                @Override
867                public void onAnimationRepeat(Animation arg0) {
868                }
869
870                @Override
871                public void onAnimationStart(Animation arg0) {
872                }
873            };
874            animationSet.setAnimationListener(listener);
875            mTemporaryImage.startAnimation(animationSet);
876        }
877    }
878
879    private void runExitAnimation() {
880        Intent intent = getIntent();
881        // FLAG: should just fall back to a standard animation if either:
882        // 1. images have been added or removed since we've been here, or
883        // 2. we are currently looking at some image other than the one we
884        // started on.
885
886        final int totalWidth = mRootView.getMeasuredWidth();
887        final int totalHeight = mRootView.getMeasuredHeight();
888
889        // We need to take a full screen image, and scale/translate it so that
890        // it appears at exactly the same location onscreen as it is in the
891        // prior activity.
892        // The final image will take either the full screen width or height (or both).
893        final float scaleW = (float) mAnimationStartWidth / totalWidth;
894        final float scaleY = (float) mAnimationStartHeight / totalHeight;
895        final float scale = Math.max(scaleW, scaleY);
896
897        final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth,
898                totalWidth, scale);
899        final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight,
900                totalHeight, scale);
901        final int version = android.os.Build.VERSION.SDK_INT;
902        if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
903            mBackground.animate().alpha(0f).setDuration(EXIT_ANIMATION_DURATION_MS).start();
904            mBackground.setVisibility(View.VISIBLE);
905
906            Runnable endRunnable = new Runnable() {
907                @Override
908                public void run() {
909                    PhotoViewActivity.this.onExitAnimationComplete();
910                }
911            };
912            // If the temporary image is still visible it means that we have
913            // not yet loaded the fullres image, so we need to animate
914            // the temporary image out.
915            ViewPropertyAnimator animator = null;
916            if (mTemporaryImage.getVisibility() == View.VISIBLE) {
917                animator = mTemporaryImage.animate().scaleX(scale).scaleY(scale)
918                    .translationX(translateX).translationY(translateY)
919                    .setDuration(EXIT_ANIMATION_DURATION_MS);
920            } else {
921                animator = mViewPager.animate().scaleX(scale).scaleY(scale)
922                    .translationX(translateX).translationY(translateY)
923                    .setDuration(EXIT_ANIMATION_DURATION_MS);
924            }
925            if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
926                animator.withEndAction(endRunnable);
927            } else {
928                mHandler.postDelayed(endRunnable, EXIT_ANIMATION_DURATION_MS);
929            }
930            animator.start();
931        } else {
932            final Animation alphaAnimation = new AlphaAnimation(1f, 0f);
933            alphaAnimation.setDuration(EXIT_ANIMATION_DURATION_MS);
934            mBackground.startAnimation(alphaAnimation);
935            mBackground.setVisibility(View.VISIBLE);
936
937            final Animation scaleAnimation = new ScaleAnimation(1f, 1f, scale, scale);
938            scaleAnimation.setDuration(EXIT_ANIMATION_DURATION_MS);
939            AnimationListener listener = new AnimationListener() {
940                @Override
941                public void onAnimationEnd(Animation arg0) {
942                    PhotoViewActivity.this.onExitAnimationComplete();
943                }
944
945                @Override
946                public void onAnimationRepeat(Animation arg0) {
947                }
948
949                @Override
950                public void onAnimationStart(Animation arg0) {
951                }
952            };
953            scaleAnimation.setAnimationListener(listener);
954            // If the temporary image is still visible it means that we have
955            // not yet loaded the fullres image, so we need to animate
956            // the temporary image out.
957            if (mTemporaryImage.getVisibility() == View.VISIBLE) {
958                mTemporaryImage.startAnimation(scaleAnimation);
959            } else {
960                mViewPager.startAnimation(scaleAnimation);
961            }
962        }
963    }
964
965    private int calculateTranslate(int start, int startSize, int totalSize, float scale) {
966        // Translation takes precedence over scale.  What this means is that if
967        // we want an view's upper left corner to be a particular spot on screen,
968        // but that view is scaled to something other than 1, we need to take into
969        // account the pixels lost to scaling.
970        // So if we have a view that is 200x300, and we want it's upper left corner
971        // to be at 50x50, but it's scaled by 50%, we can't just translate it to 50x50.
972        // If we were to do that, the view's *visible* upper left corner would be at
973        // 100x200.  We need to take into account the difference between the outside
974        // size of the view (i.e. the size prior to scaling) and the scaled size.
975        // scaleFromEdge is the difference between the visible left edge and the
976        // actual left edge, due to scaling.
977        // scaleFromTop is the difference between the visible top edge, and the
978        // actual top edge, due to scaling.
979        int scaleFromEdge = Math.round((totalSize - totalSize * scale) / 2);
980
981        // The imageView is fullscreen, regardless of the aspect ratio of the actual image.
982        // This means that some portion of the imageView will be blank.  We need to
983        // take into account the size of the blank area so that the actual image
984        // lines up with the starting image.
985        int blankSize = Math.round((totalSize * scale - startSize) / 2);
986
987        return start - scaleFromEdge - blankSize;
988    }
989
990    private void initTemporaryImage(Drawable drawable) {
991        if (mEnterAnimationFinished) {
992            // Forget this, we've already run the animation.
993            return;
994        }
995        mTemporaryImage.setImageDrawable(drawable);
996        if (drawable != null) {
997            // We have not yet run the enter animation. Start it now.
998            int totalWidth = mRootView.getMeasuredWidth();
999            if (totalWidth == 0) {
1000                // the measure pass has not yet finished.  We can't properly
1001                // run out animation until that is done. Listen for the layout
1002                // to occur, then fire the animation.
1003                final View base = mRootView;
1004                base.getViewTreeObserver().addOnGlobalLayoutListener(
1005                        new OnGlobalLayoutListener() {
1006                    @Override
1007                    public void onGlobalLayout() {
1008                        int version = android.os.Build.VERSION.SDK_INT;
1009                        if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
1010                            base.getViewTreeObserver().removeOnGlobalLayoutListener(this);
1011                        } else {
1012                            base.getViewTreeObserver().removeGlobalOnLayoutListener(this);
1013                        }
1014                        runEnterAnimation();
1015                    }
1016                });
1017            } else {
1018                // initiate the animation
1019                runEnterAnimation();
1020            }
1021        }
1022        // Kick off the photo list loader
1023        getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
1024    }
1025
1026    private class BitmapCallback implements LoaderManager.LoaderCallbacks<BitmapResult> {
1027
1028        @Override
1029        public Loader<BitmapResult> onCreateLoader(int id, Bundle args) {
1030            String uri = args.getString(ARG_IMAGE_URI);
1031            switch (id) {
1032                case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL:
1033                    return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
1034                            args, uri);
1035                case PhotoViewCallbacks.BITMAP_LOADER_AVATAR:
1036                    return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_AVATAR,
1037                            args, uri);
1038            }
1039            return null;
1040        }
1041
1042        @Override
1043        public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) {
1044            Drawable drawable = result.getDrawable(getResources());
1045            final ActionBar actionBar = getSupportActionBar();
1046            switch (loader.getId()) {
1047                case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL:
1048                    // We just loaded the initial thumbnail that we can display
1049                    // while waiting for the full viewPager to get initialized.
1050                    initTemporaryImage(drawable);
1051                    // Destroy the loader so we don't attempt to load the thumbnail
1052                    // again on screen rotations.
1053                    getSupportLoaderManager().destroyLoader(
1054                            PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL);
1055                    break;
1056                case PhotoViewCallbacks.BITMAP_LOADER_AVATAR:
1057                    if (drawable == null) {
1058                        actionBar.setLogo(null);
1059                    } else {
1060                        actionBar.setLogo(drawable);
1061                    }
1062                    break;
1063            }
1064        }
1065
1066        @Override
1067        public void onLoaderReset(Loader<BitmapResult> loader) {
1068            // Do nothing
1069        }
1070    }
1071}
1072