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