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