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.Activity;
     21 import android.app.ActivityManager;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.res.Resources;
     25 import android.database.Cursor;
     26 import android.graphics.Bitmap;
     27 import android.graphics.drawable.BitmapDrawable;
     28 import android.net.Uri;
     29 import android.os.Build;
     30 import android.os.Bundle;
     31 import android.os.Handler;
     32 import android.support.v4.app.Fragment;
     33 import android.support.v4.app.LoaderManager;
     34 import android.support.v4.content.Loader;
     35 import android.support.v4.view.ViewPager.OnPageChangeListener;
     36 import android.view.MenuItem;
     37 import android.support.v7.app.ActionBar;
     38 import android.support.v7.app.ActionBar.OnMenuVisibilityListener;
     39 import android.support.v7.app.ActionBarActivity;
     40 import android.text.TextUtils;
     41 import android.util.Log;
     42 import android.view.View;
     43 import android.view.ViewPropertyAnimator;
     44 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     45 import android.view.animation.AlphaAnimation;
     46 import android.view.animation.Animation;
     47 import android.view.animation.AnimationSet;
     48 import android.view.animation.ScaleAnimation;
     49 import android.view.animation.TranslateAnimation;
     50 import android.view.animation.Animation.AnimationListener;
     51 import android.widget.ImageView;
     52 
     53 import com.android.ex.photo.PhotoViewPager.InterceptType;
     54 import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener;
     55 import com.android.ex.photo.adapters.PhotoPagerAdapter;
     56 import com.android.ex.photo.fragments.PhotoViewFragment;
     57 import com.android.ex.photo.loaders.PhotoBitmapLoader;
     58 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
     59 import com.android.ex.photo.loaders.PhotoPagerLoader;
     60 import com.android.ex.photo.provider.PhotoContract;
     61 
     62 import java.util.HashMap;
     63 import java.util.HashSet;
     64 import java.util.Map;
     65 import java.util.Set;
     66 
     67 /**
     68  * Activity to view the contents of an album.
     69  */
     70 public 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