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