Home | History | Annotate | Download | only in photo
      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 
     18 package com.android.ex.photo;
     19 
     20 import android.app.ActionBar;
     21 import android.app.ActionBar.OnMenuVisibilityListener;
     22 import android.app.Activity;
     23 import android.app.ActivityManager;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.res.Resources;
     27 import android.database.Cursor;
     28 import android.graphics.Bitmap;
     29 import android.graphics.drawable.BitmapDrawable;
     30 import android.net.Uri;
     31 import android.os.Build;
     32 import android.os.Bundle;
     33 import android.os.Handler;
     34 import android.support.v4.app.Fragment;
     35 import android.support.v4.app.FragmentActivity;
     36 import android.support.v4.app.LoaderManager;
     37 import android.support.v4.content.Loader;
     38 import android.support.v4.view.ViewPager.OnPageChangeListener;
     39 import android.view.MenuItem;
     40 
     41 import android.text.TextUtils;
     42 import android.util.Log;
     43 import android.view.View;
     44 import android.view.ViewPropertyAnimator;
     45 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     46 import android.view.animation.AlphaAnimation;
     47 import android.view.animation.Animation;
     48 import android.view.animation.AnimationSet;
     49 import android.view.animation.ScaleAnimation;
     50 import android.view.animation.TranslateAnimation;
     51 import android.view.animation.Animation.AnimationListener;
     52 import android.widget.ImageView;
     53 
     54 import com.android.ex.photo.PhotoViewPager.InterceptType;
     55 import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener;
     56 import com.android.ex.photo.adapters.PhotoPagerAdapter;
     57 import com.android.ex.photo.fragments.PhotoViewFragment;
     58 import com.android.ex.photo.loaders.PhotoBitmapLoader;
     59 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
     60 import com.android.ex.photo.loaders.PhotoPagerLoader;
     61 import com.android.ex.photo.provider.PhotoContract;
     62 
     63 import java.util.HashMap;
     64 import java.util.HashSet;
     65 import java.util.Map;
     66 import java.util.Set;
     67 
     68 /**
     69  * Activity to view the contents of an album.
     70  */
     71 public class PhotoViewActivity extends FragmentActivity 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 = getActionBar();
    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 = getActionBar();
    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(getActionBar());
    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(Bitmap bitmap) {
    991         if (mEnterAnimationFinished) {
    992             // Forget this, we've already run the animation.
    993             return;
    994         }
    995         mTemporaryImage.setImageBitmap(bitmap);
    996         if (bitmap != 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             Bitmap bitmap = result.bitmap;
   1045             final ActionBar actionBar = getActionBar();
   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(bitmap);
   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 (bitmap == null) {
   1058                         actionBar.setLogo(null);
   1059                     } else {
   1060                         BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap);
   1061                         actionBar.setLogo(drawable);
   1062                     }
   1063                     break;
   1064             }
   1065         }
   1066 
   1067         @Override
   1068         public void onLoaderReset(Loader<BitmapResult> loader) {
   1069             // Do nothing
   1070         }
   1071     }
   1072 }
   1073