FileDocCategorySizeDatePackage
PhotoViewController.javaAPI DocAndroid 5.1 API52999Thu Mar 12 22:22:52 GMT 2015com.android.ex.photo

PhotoViewController

public class PhotoViewController extends Object implements PhotoViewCallbacks, com.android.ex.photo.ActionBarInterface.OnMenuVisibilityListener, LoaderManager.LoaderCallbacks, android.support.v4.view.ViewPager.OnPageChangeListener, com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener
This class implements all the logic of the photo view activity. An activity should use this class calling through from relevant activity methods to the methods of the same name here. To customize the photo viewer activity, you should subclass this and implement your customizations here. Then subclass {@link PhotoViewActivity} and override just {@link PhotoViewActivity#createController createController} to instantiate your controller subclass.

Fields Summary
private static final String
TAG
private static final String
STATE_INITIAL_URI_KEY
private static final String
STATE_CURRENT_URI_KEY
private static final String
STATE_CURRENT_INDEX_KEY
private static final String
STATE_FULLSCREEN_KEY
private static final String
STATE_ACTIONBARTITLE_KEY
private static final String
STATE_ACTIONBARSUBTITLE_KEY
private static final String
STATE_ENTERANIMATIONFINISHED_KEY
protected static final String
ARG_IMAGE_URI
public static final int
LOADER_PHOTO_LIST
public static final int
ALBUM_COUNT_UNKNOWN
Count used when the real photo count is unknown [but, may be determined]
public static final int
ENTER_ANIMATION_DURATION_MS
public static final int
EXIT_ANIMATION_DURATION_MS
public static final String
KEY_MESSAGE
Argument key for the dialog message
public static int
sMemoryClass
public static int
sMaxPhotoSize
private final ActivityInterface
mActivity
private int
mLastFlags
private final View.OnSystemUiVisibilityChangeListener
mSystemUiVisibilityChangeListener
private String
mPhotosUri
The URI of the photos we're viewing; may be {@code null}
private String
mInitialPhotoUri
The uri of the initial photo
private int
mCurrentPhotoIndex
The index of the currently viewed photo
private String
mCurrentPhotoUri
The uri of the currently viewed photo
private String[]
mProjection
The query projection to use; may be {@code null}
protected int
mAlbumCount
The total number of photos; only valid if {@link #mIsEmpty} is {@code false}.
protected boolean
mIsEmpty
{@code true} if the view is empty. Otherwise, {@code false}.
protected android.view.View
mRootView
the main root view
protected android.view.View
mBackground
Background image that contains nothing, so it can be alpha faded from transparent to black without affecting any other views.
protected PhotoViewPager
mViewPager
The main pager; provides left/right swipe between photos
protected android.widget.ImageView
mTemporaryImage
The temporary image so that we can quickly scale up the fullscreen thumbnail
protected com.android.ex.photo.adapters.PhotoPagerAdapter
mAdapter
Adapter to create pager views
protected boolean
mFullScreen
Whether or not we're in "full screen" mode
private final Map
mScreenListeners
The listeners wanting full screen state for each screen position
private final Set
mCursorListeners
The set of listeners wanting full screen state
private boolean
mKickLoader
When {@code true}, restart the loader when the activity becomes active
private boolean
mIsDestroyedCompat
Don't attempt operations that may trigger a fragment transaction when the activity is destroyed
protected boolean
mIsPaused
Whether or not this activity is paused
protected float
mMaxInitialScale
The maximum scale factor applied to images when they are initially displayed
protected String
mActionBarTitle
The title in the actionbar
protected String
mActionBarSubtitle
The subtitle in the actionbar
private boolean
mEnterAnimationFinished
protected boolean
mScaleAnimationEnabled
protected int
mAnimationStartX
protected int
mAnimationStartY
protected int
mAnimationStartWidth
protected int
mAnimationStartHeight
protected boolean
mActionBarHiddenInitially
protected boolean
mDisplayThumbsFullScreen
private final android.view.accessibility.AccessibilityManager
mAccessibilityManager
protected BitmapCallback
mBitmapCallback
protected final android.os.Handler
mHandler
private long
mEnterFullScreenDelayTime
private final Runnable
mEnterFullScreenRunnable
Constructors Summary
public PhotoViewController(ActivityInterface activity)


       
        mActivity = activity;

        // View.OnSystemUiVisibilityChangeListener is an API that was introduced in API level 11.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            mSystemUiVisibilityChangeListener = null;
        } else {
            mSystemUiVisibilityChangeListener = new View.OnSystemUiVisibilityChangeListener() {
                @Override
                public void onSystemUiVisibilityChange(int visibility) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
                            visibility == 0 && mLastFlags == 3846) {
                        setFullScreen(false /* fullscreen */, true /* setDelayedRunnable */);
                    }
                }
            };
        }

        mAccessibilityManager = (AccessibilityManager)
                activity.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
    
Methods Summary
public synchronized voidaddCursorListener(CursorChangedListener listener)

        mCursorListeners.add(listener);
    
public voidaddScreenListener(int position, OnScreenListener listener)

        mScreenListeners.put(position, listener);
    
private intcalculateTranslate(int start, int startSize, int totalSize, float scale)

        // Translation takes precedence over scale.  What this means is that if
        // we want an view's upper left corner to be a particular spot on screen,
        // but that view is scaled to something other than 1, we need to take into
        // account the pixels lost to scaling.
        // So if we have a view that is 200x300, and we want it's upper left corner
        // to be at 50x50, but it's scaled by 50%, we can't just translate it to 50x50.
        // If we were to do that, the view's *visible* upper left corner would be at
        // 100x200.  We need to take into account the difference between the outside
        // size of the view (i.e. the size prior to scaling) and the scaled size.
        // scaleFromEdge is the difference between the visible left edge and the
        // actual left edge, due to scaling.
        // scaleFromTop is the difference between the visible top edge, and the
        // actual top edge, due to scaling.
        int scaleFromEdge = Math.round((totalSize - totalSize * scale) / 2);

        // The imageView is fullscreen, regardless of the aspect ratio of the actual image.
        // This means that some portion of the imageView will be blank.  We need to
        // take into account the size of the blank area so that the actual image
        // lines up with the starting image.
        int blankSize = Math.round((totalSize * scale - startSize) / 2);

        return start - scaleFromEdge - blankSize;
    
private voidcancelEnterFullScreenRunnable()

        mHandler.removeCallbacks(mEnterFullScreenRunnable);
    
public com.android.ex.photo.adapters.PhotoPagerAdaptercreatePhotoPagerAdapter(android.content.Context context, android.support.v4.app.FragmentManager fm, android.database.Cursor c, float maxScale)

        return new PhotoPagerAdapter(context, fm, c, maxScale, mDisplayThumbsFullScreen);
    
protected android.view.ViewfindViewById(int id)

        return mActivity.findViewById(id);
    
public com.android.ex.photo.PhotoViewController$ActivityInterfacegetActivity()

        return mActivity;
    
public com.android.ex.photo.adapters.PhotoPagerAdaptergetAdapter()

        return mAdapter;
    
public android.database.CursorgetCursor()

        return (mAdapter == null) ? null : mAdapter.getCursor();
    
public android.database.CursorgetCursorAtProperPosition()
Utility method that will return the cursor that contains the data at the current position so that it refers to the current image on screen.

return
the cursor at the current position or null if no cursor exists or if the {@link PhotoViewPager} is null.

        if (mViewPager == null) {
            return null;
        }

        final int position = mViewPager.getCurrentItem();
        final Cursor cursor = mAdapter.getCursor();

        if (cursor == null) {
            return null;
        }

        cursor.moveToPosition(position);

        return cursor;
    
private static final java.lang.StringgetInputOrEmpty(java.lang.String in)
If the input string is non-null, it is returned, otherwise an empty string is returned;

param
in
return

        if (in == null) {
            return "";
        }
        return in;
    
protected java.lang.StringgetPhotoAccessibilityAnnouncement(int position)
Returns a string used as an announcement for accessibility after the user moves to a new photo. It will be called after {@link #updateActionBar} has been called.

param
position the index in the album of the currently active photo
return
announcement for accessibility

        String announcement = mActionBarTitle;
        if (mActionBarSubtitle != null) {
            announcement = mActivity.getContext().getResources().getString(
                    R.string.titles, mActionBarTitle, mActionBarSubtitle);
        }
        return announcement;
    
public android.view.ViewgetRootView()

        return mRootView;
    
public View.OnSystemUiVisibilityChangeListenergetSystemUiVisibilityChangeListener()
Note: This should only be called when API level is 11 or above.

        return mSystemUiVisibilityChangeListener;
    
public voidhideActionBar()

        mActivity.getActionBarInterface().hide();
    
private voidinitMaxPhotoSize()

        if (sMaxPhotoSize == 0) {
            final DisplayMetrics metrics = new DisplayMetrics();
            final WindowManager wm = (WindowManager)
                    mActivity.getContext().getSystemService(Context.WINDOW_SERVICE);
            final ImageUtils.ImageSize imageSize = ImageUtils.sUseImageSize;
            wm.getDefaultDisplay().getMetrics(metrics);
            switch (imageSize) {
                case EXTRA_SMALL:
                    // Use a photo that's 80% of the "small" size
                    sMaxPhotoSize = (Math.min(metrics.heightPixels, metrics.widthPixels) * 800) / 1000;
                    break;
                case SMALL:
                    // Fall through.
                case NORMAL:
                    // Fall through.
                default:
                    sMaxPhotoSize = Math.min(metrics.heightPixels, metrics.widthPixels);
                    break;
            }
        }
    
private voidinitTemporaryImage(android.graphics.drawable.Drawable drawable)

        if (mEnterAnimationFinished) {
            // Forget this, we've already run the animation.
            return;
        }
        mTemporaryImage.setImageDrawable(drawable);
        if (drawable != null) {
            // We have not yet run the enter animation. Start it now.
            int totalWidth = mRootView.getMeasuredWidth();
            if (totalWidth == 0) {
                // the measure pass has not yet finished.  We can't properly
                // run out animation until that is done. Listen for the layout
                // to occur, then fire the animation.
                final View base = mRootView;
                base.getViewTreeObserver().addOnGlobalLayoutListener(
                        new OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        int version = android.os.Build.VERSION.SDK_INT;
                        if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
                            base.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                        } else {
                            base.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                        }
                        runEnterAnimation();
                    }
                });
            } else {
                // initiate the animation
                runEnterAnimation();
            }
        }
        // Kick off the photo list loader
        mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
    
private booleanisDestroyedCompat()

        return mIsDestroyedCompat;
    
public booleanisEnterAnimationFinished()

        return mEnterAnimationFinished;
    
public booleanisFragmentActive(android.support.v4.app.Fragment fragment)

        if (mViewPager == null || mAdapter == null) {
            return false;
        }
        return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment);
    
public booleanisFragmentFullScreen(android.support.v4.app.Fragment fragment)

        if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) {
            return mFullScreen;
        }
        return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment));
    
protected booleanisFullScreen()

        return mFullScreen;
    
public booleanisScaleAnimationEnabled()

        return mScaleAnimationEnabled;
    
private booleankitkatIsSecondaryUser()
Return true iff the app is being run as a secondary user on kitkat. This is a hack which we only know to work on kitkat.

        if (Build.VERSION.SDK_INT != Build.VERSION_CODES.KITKAT) {
            throw new IllegalStateException("kitkatIsSecondary user is only callable on KitKat");
        }
        return Process.myUid() > 100000;
    
private synchronized voidnotifyCursorListeners(android.database.Cursor data)

        // tell all of the objects listening for cursor changes
        // that the cursor has changed
        for (CursorChangedListener listener : mCursorListeners) {
            listener.onCursorChanged(data);
        }
    
public voidonActivityResult(int requestCode, int resultCode, android.content.Intent data)

public booleanonBackPressed()

        // If we are in fullscreen mode, and the default is not full screen, then
        // switch back to actionBar display mode.
        if (mFullScreen && !mActionBarHiddenInitially) {
            toggleFullScreen();
        } else {
            if (mScaleAnimationEnabled) {
                runExitAnimation();
            } else {
                return false;
            }
        }
        return true;
    
public voidonCreate(android.os.Bundle savedInstanceState)

        initMaxPhotoSize();
        final ActivityManager mgr = (ActivityManager) mActivity.getApplicationContext().
                getSystemService(Activity.ACTIVITY_SERVICE);
        sMemoryClass = mgr.getMemoryClass();

        final Intent intent = mActivity.getIntent();
        // uri of the photos to view; optional
        if (intent.hasExtra(Intents.EXTRA_PHOTOS_URI)) {
            mPhotosUri = intent.getStringExtra(Intents.EXTRA_PHOTOS_URI);
        }
        if (intent.getBooleanExtra(Intents.EXTRA_SCALE_UP_ANIMATION, false)) {
            mScaleAnimationEnabled = true;
            mAnimationStartX = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_X, 0);
            mAnimationStartY = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_Y, 0);
            mAnimationStartWidth = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_WIDTH, 0);
            mAnimationStartHeight = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_HEIGHT, 0);
        }
        mActionBarHiddenInitially = intent.getBooleanExtra(
                Intents.EXTRA_ACTION_BAR_HIDDEN_INITIALLY, false)
                && !Util.isTouchExplorationEnabled(mAccessibilityManager);
        mDisplayThumbsFullScreen = intent.getBooleanExtra(
                Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false);

        // projection for the query; optional
        // If not set, the default projection is used.
        // This projection must include the columns from the default projection.
        if (intent.hasExtra(Intents.EXTRA_PROJECTION)) {
            mProjection = intent.getStringArrayExtra(Intents.EXTRA_PROJECTION);
        } else {
            mProjection = null;
        }

        // Set the max initial scale, defaulting to 1x
        mMaxInitialScale = intent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f);
        mCurrentPhotoUri = null;
        mCurrentPhotoIndex = -1;

        // We allow specifying the current photo by either index or uri.
        // This is because some users may have live datasets that can change,
        // adding new items to either the beginning or end of the set. For clients
        // that do not need that capability, ability to specify the current photo
        // by index is offered as a convenience.
        if (intent.hasExtra(Intents.EXTRA_PHOTO_INDEX)) {
            mCurrentPhotoIndex = intent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1);
        }
        if (intent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI)) {
            mInitialPhotoUri = intent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI);
            mCurrentPhotoUri = mInitialPhotoUri;
        }
        mIsEmpty = true;

        if (savedInstanceState != null) {
            mInitialPhotoUri = savedInstanceState.getString(STATE_INITIAL_URI_KEY);
            mCurrentPhotoUri = savedInstanceState.getString(STATE_CURRENT_URI_KEY);
            mCurrentPhotoIndex = savedInstanceState.getInt(STATE_CURRENT_INDEX_KEY);
            mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false)
                    && !Util.isTouchExplorationEnabled(mAccessibilityManager);
            mActionBarTitle = savedInstanceState.getString(STATE_ACTIONBARTITLE_KEY);
            mActionBarSubtitle = savedInstanceState.getString(STATE_ACTIONBARSUBTITLE_KEY);
            mEnterAnimationFinished = savedInstanceState.getBoolean(
                    STATE_ENTERANIMATIONFINISHED_KEY, false);
        } else {
            mFullScreen = mActionBarHiddenInitially;
        }

        mActivity.setContentView(R.layout.photo_activity_view);

        // Create the adapter and add the view pager
        mAdapter = createPhotoPagerAdapter(mActivity.getContext(),
                        mActivity.getSupportFragmentManager(), null, mMaxInitialScale);
        final Resources resources = mActivity.getResources();
        mRootView = findViewById(R.id.photo_activity_root_view);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            mRootView.setOnSystemUiVisibilityChangeListener(getSystemUiVisibilityChangeListener());
        }
        mBackground = findViewById(R.id.photo_activity_background);
        mTemporaryImage = (ImageView) findViewById(R.id.photo_activity_temporary_image);
        mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager);
        mViewPager.setAdapter(mAdapter);
        mViewPager.setOnPageChangeListener(this);
        mViewPager.setOnInterceptTouchListener(this);
        mViewPager.setPageMargin(resources.getDimensionPixelSize(R.dimen.photo_page_margin));

        mBitmapCallback = new BitmapCallback();
        if (!mScaleAnimationEnabled || mEnterAnimationFinished) {
            // We are not running the scale up animation. Just let the fragments
            // display and handle the animation.
            mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
            // Make the background opaque immediately so that we don't see the activity
            // behind this one.
            mBackground.setVisibility(View.VISIBLE);
        } else {
            // Attempt to load the initial image thumbnail. Once we have the
            // image, animate it up. Once the animation is complete, we can kick off
            // loading the ViewPager. After the primary fullres image is loaded, we will
            // make our temporary image invisible and display the ViewPager.
            mViewPager.setVisibility(View.GONE);
            Bundle args = new Bundle();
            args.putString(ARG_IMAGE_URI, mInitialPhotoUri);
            mActivity.getSupportLoaderManager().initLoader(
                    BITMAP_LOADER_THUMBNAIL, args, mBitmapCallback);
        }

        mEnterFullScreenDelayTime =
                resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis);

        final ActionBarInterface actionBar = mActivity.getActionBarInterface();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.addOnMenuVisibilityListener(this);
            actionBar.setDisplayOptionsShowTitle();
            // Set the title and subtitle immediately here, rather than waiting
            // for the fragment to be initialized.
            setActionBarTitles(actionBar);
        }

        if (!mScaleAnimationEnabled) {
            setLightsOutMode(mFullScreen);
        } else {
            // Keep lights out mode as false. This is to prevent jank cause by concurrent
            // animations during the enter animation.
            setLightsOutMode(false);
        }
    
public android.support.v4.content.LoaderonCreateBitmapLoader(int id, android.os.Bundle args, java.lang.String uri)

        switch (id) {
            case BITMAP_LOADER_AVATAR:
            case BITMAP_LOADER_THUMBNAIL:
            case BITMAP_LOADER_PHOTO:
                return new PhotoBitmapLoader(mActivity.getContext(), uri);
            default:
                return null;
        }
    
public android.support.v4.content.LoaderonCreateLoader(int id, android.os.Bundle args)

        if (id == LOADER_PHOTO_LIST) {
            return new PhotoPagerLoader(mActivity.getContext(), Uri.parse(mPhotosUri), mProjection);
        }
        return null;
    
public booleanonCreateOptionsMenu(android.view.Menu menu)

        return true;
    
public voidonCursorChanged(com.android.ex.photo.fragments.PhotoViewFragment fragment, android.database.Cursor cursor)

        // do nothing
    
public voidonDestroy()

        mIsDestroyedCompat = true;
    
public voidonEnterAnimationComplete()

        mEnterAnimationFinished = true;
        mViewPager.setVisibility(View.VISIBLE);
        setLightsOutMode(mFullScreen);
    
private voidonExitAnimationComplete()

        mActivity.finish();
        mActivity.overridePendingTransition(0, 0);
    
public voidonFragmentPhotoLoadComplete(com.android.ex.photo.fragments.PhotoViewFragment fragment, boolean success)

        if (mTemporaryImage.getVisibility() != View.GONE &&
                TextUtils.equals(fragment.getPhotoUri(), mCurrentPhotoUri)) {
            if (success) {
                // The fragment for the current image is now ready for display.
                mTemporaryImage.setVisibility(View.GONE);
                mViewPager.setVisibility(View.VISIBLE);
            } else {
                // This means that we are unable to load the fragment's photo.
                // I'm not sure what the best thing to do here is, but at least if
                // we display the viewPager, the fragment itself can decide how to
                // display the failure of its own image.
                Log.w(TAG, "Failed to load fragment image");
                mTemporaryImage.setVisibility(View.GONE);
                mViewPager.setVisibility(View.VISIBLE);
            }
            mActivity.getSupportLoaderManager().destroyLoader(
                    PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL);
        }
    
public voidonFragmentVisible(com.android.ex.photo.fragments.PhotoViewFragment fragment)

        // Do nothing, we handle this in setViewActivated
    
public voidonLoadFinished(android.support.v4.content.Loader loader, android.database.Cursor data)

        final int id = loader.getId();
        if (id == LOADER_PHOTO_LIST) {
            if (data == null || data.getCount() == 0) {
                mIsEmpty = true;
                mAdapter.swapCursor(null);
            } else {
                mAlbumCount = data.getCount();
                if (mCurrentPhotoUri != null) {
                    int index = 0;
                    // Clear query params. Compare only the path.
                    final int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
                    final Uri currentPhotoUri;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                        currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon()
                                .clearQuery().build();
                    } else {
                        currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon()
                                .query(null).build();
                    }
                    // Rewind data cursor to the start if it has already advanced.
                    data.moveToPosition(-1);
                    while (data.moveToNext()) {
                        final String uriString = data.getString(uriIndex);
                        final Uri uri;
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                            uri = Uri.parse(uriString).buildUpon().clearQuery().build();
                        } else {
                            uri = Uri.parse(uriString).buildUpon().query(null).build();
                        }
                        if (currentPhotoUri != null && currentPhotoUri.equals(uri)) {
                            mCurrentPhotoIndex = index;
                            break;
                        }
                        index++;
                    }
                }

                // We're paused; don't do anything now, we'll get re-invoked
                // when the activity becomes active again
                if (mIsPaused) {
                    mKickLoader = true;
                    mAdapter.swapCursor(null);
                    return;
                }
                boolean wasEmpty = mIsEmpty;
                mIsEmpty = false;

                mAdapter.swapCursor(data);
                if (mViewPager.getAdapter() == null) {
                    mViewPager.setAdapter(mAdapter);
                }
                notifyCursorListeners(data);

                // Use an index of 0 if the index wasn't specified or couldn't be found
                if (mCurrentPhotoIndex < 0) {
                    mCurrentPhotoIndex = 0;
                }

                mViewPager.setCurrentItem(mCurrentPhotoIndex, false);
                if (wasEmpty) {
                    setViewActivated(mCurrentPhotoIndex);
                }
            }
            // Update the any action items
            updateActionItems();
        }
    
public voidonLoaderReset(android.support.v4.content.Loader loader)

        // If the loader is reset, remove the reference in the adapter to this cursor
        if (!isDestroyedCompat()) {
            // This will cause a fragment transaction which can't happen if we're destroyed,
            // but we don't care in that case because we're destroyed anyways.
            mAdapter.swapCursor(null);
        }
    
public voidonMenuVisibilityChanged(boolean isVisible)

        if (isVisible) {
            cancelEnterFullScreenRunnable();
        } else {
            postEnterFullScreenRunnableWithDelay();
        }
    
public voidonNewPhotoLoaded(int position)

        // do nothing
    
public booleanonOptionsItemSelected(android.view.MenuItem item)

       switch (item.getItemId()) {
          case android.R.id.home:
             mActivity.finish();
             return true;
          default:
             return false;
       }
    
public voidonPageScrollStateChanged(int state)

    
public voidonPageScrolled(int position, float positionOffset, int positionOffsetPixels)

        if (positionOffset < 0.0001) {
            OnScreenListener before = mScreenListeners.get(position - 1);
            if (before != null) {
                before.onViewUpNext();
            }
            OnScreenListener after = mScreenListeners.get(position + 1);
            if (after != null) {
                after.onViewUpNext();
            }
        }
    
public voidonPageSelected(int position)

        mCurrentPhotoIndex = position;
        setViewActivated(position);
    
public voidonPause()

        mIsPaused = true;
    
public voidonPhotoRemoved(long photoId)

        final Cursor data = mAdapter.getCursor();
        if (data == null) {
            // Huh?! How would this happen?
            return;
        }

        final int dataCount = data.getCount();
        if (dataCount <= 1) {
            mActivity.finish();
            return;
        }

        mActivity.getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this);
    
public booleanonPrepareOptionsMenu(android.view.Menu menu)

        return true;
    
public voidonResume()

        setFullScreen(mFullScreen, false);

        mIsPaused = false;
        if (mKickLoader) {
            mKickLoader = false;
            mActivity.getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
        }
    
public voidonSaveInstanceState(android.os.Bundle outState)

        outState.putString(STATE_INITIAL_URI_KEY, mInitialPhotoUri);
        outState.putString(STATE_CURRENT_URI_KEY, mCurrentPhotoUri);
        outState.putInt(STATE_CURRENT_INDEX_KEY, mCurrentPhotoIndex);
        outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen);
        outState.putString(STATE_ACTIONBARTITLE_KEY, mActionBarTitle);
        outState.putString(STATE_ACTIONBARSUBTITLE_KEY, mActionBarSubtitle);
        outState.putBoolean(STATE_ENTERANIMATIONFINISHED_KEY, mEnterAnimationFinished);
    
public voidonStart()

public voidonStop()

public com.android.ex.photo.PhotoViewPager.InterceptTypeonTouchIntercept(float origX, float origY)

        boolean interceptLeft = false;
        boolean interceptRight = false;

        for (OnScreenListener listener : mScreenListeners.values()) {
            if (!interceptLeft) {
                interceptLeft = listener.onInterceptMoveLeft(origX, origY);
            }
            if (!interceptRight) {
                interceptRight = listener.onInterceptMoveRight(origX, origY);
            }
        }

        if (interceptLeft) {
            if (interceptRight) {
                return InterceptType.BOTH;
            }
            return InterceptType.LEFT;
        } else if (interceptRight) {
            return InterceptType.RIGHT;
        }
        return InterceptType.NONE;
    
private voidpostEnterFullScreenRunnableWithDelay()

        mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime);
    
public synchronized voidremoveCursorListener(CursorChangedListener listener)

        mCursorListeners.remove(listener);
    
public voidremoveScreenListener(int position)

        mScreenListeners.remove(position);
    
private voidrunEnterAnimation()

        final int totalWidth = mRootView.getMeasuredWidth();
        final int totalHeight = mRootView.getMeasuredHeight();

        // FLAG: Need to handle the aspect ratio of the bitmap.  If it's a portrait
        // bitmap, then we need to position the view higher so that the middle
        // pixels line up.
        mTemporaryImage.setVisibility(View.VISIBLE);
        // We need to take a full screen image, and scale/translate it so that
        // it appears at exactly the same location onscreen as it is in the
        // prior activity.
        // The final image will take either the full screen width or height (or both).

        final float scaleW = (float) mAnimationStartWidth / totalWidth;
        final float scaleY = (float) mAnimationStartHeight / totalHeight;
        final float scale = Math.max(scaleW, scaleY);

        final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth,
                totalWidth, scale);
        final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight,
                totalHeight, scale);

        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            mBackground.setAlpha(0f);
            mBackground.animate().alpha(1f).setDuration(ENTER_ANIMATION_DURATION_MS).start();
            mBackground.setVisibility(View.VISIBLE);

            mTemporaryImage.setScaleX(scale);
            mTemporaryImage.setScaleY(scale);
            mTemporaryImage.setTranslationX(translateX);
            mTemporaryImage.setTranslationY(translateY);

            Runnable endRunnable = new Runnable() {
                @Override
                public void run() {
                    PhotoViewController.this.onEnterAnimationComplete();
                }
            };
            ViewPropertyAnimator animator = mTemporaryImage.animate().scaleX(1f).scaleY(1f)
                .translationX(0).translationY(0).setDuration(ENTER_ANIMATION_DURATION_MS);
            if (version >= Build.VERSION_CODES.JELLY_BEAN) {
                animator.withEndAction(endRunnable);
            } else {
                mHandler.postDelayed(endRunnable, ENTER_ANIMATION_DURATION_MS);
            }
            animator.start();
        } else {
            final Animation alphaAnimation = new AlphaAnimation(0f, 1f);
            alphaAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);
            mBackground.startAnimation(alphaAnimation);
            mBackground.setVisibility(View.VISIBLE);

            final Animation translateAnimation = new TranslateAnimation(translateX,
                    translateY, 0, 0);
            translateAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);
            Animation scaleAnimation = new ScaleAnimation(scale, scale, 0, 0);
            scaleAnimation.setDuration(ENTER_ANIMATION_DURATION_MS);

            AnimationSet animationSet = new AnimationSet(true);
            animationSet.addAnimation(translateAnimation);
            animationSet.addAnimation(scaleAnimation);
            AnimationListener listener = new AnimationListener() {
                @Override
                public void onAnimationEnd(Animation arg0) {
                    PhotoViewController.this.onEnterAnimationComplete();
                }

                @Override
                public void onAnimationRepeat(Animation arg0) {
                }

                @Override
                public void onAnimationStart(Animation arg0) {
                }
            };
            animationSet.setAnimationListener(listener);
            mTemporaryImage.startAnimation(animationSet);
        }
    
private voidrunExitAnimation()

        Intent intent = mActivity.getIntent();
        // FLAG: should just fall back to a standard animation if either:
        // 1. images have been added or removed since we've been here, or
        // 2. we are currently looking at some image other than the one we
        // started on.

        final int totalWidth = mRootView.getMeasuredWidth();
        final int totalHeight = mRootView.getMeasuredHeight();

        // We need to take a full screen image, and scale/translate it so that
        // it appears at exactly the same location onscreen as it is in the
        // prior activity.
        // The final image will take either the full screen width or height (or both).
        final float scaleW = (float) mAnimationStartWidth / totalWidth;
        final float scaleY = (float) mAnimationStartHeight / totalHeight;
        final float scale = Math.max(scaleW, scaleY);

        final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth,
                totalWidth, scale);
        final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight,
                totalHeight, scale);
        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            mBackground.animate().alpha(0f).setDuration(EXIT_ANIMATION_DURATION_MS).start();
            mBackground.setVisibility(View.VISIBLE);

            Runnable endRunnable = new Runnable() {
                @Override
                public void run() {
                    PhotoViewController.this.onExitAnimationComplete();
                }
            };
            // If the temporary image is still visible it means that we have
            // not yet loaded the fullres image, so we need to animate
            // the temporary image out.
            ViewPropertyAnimator animator = null;
            if (mTemporaryImage.getVisibility() == View.VISIBLE) {
                animator = mTemporaryImage.animate().scaleX(scale).scaleY(scale)
                    .translationX(translateX).translationY(translateY)
                    .setDuration(EXIT_ANIMATION_DURATION_MS);
            } else {
                animator = mViewPager.animate().scaleX(scale).scaleY(scale)
                    .translationX(translateX).translationY(translateY)
                    .setDuration(EXIT_ANIMATION_DURATION_MS);
            }
            // If the user has swiped to a different photo, fade out the current photo
            // along with the scale animation.
            if (!mInitialPhotoUri.equals(mCurrentPhotoUri)) {
                animator.alpha(0f);
            }
            if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
                animator.withEndAction(endRunnable);
            } else {
                mHandler.postDelayed(endRunnable, EXIT_ANIMATION_DURATION_MS);
            }
            animator.start();
        } else {
            final Animation alphaAnimation = new AlphaAnimation(1f, 0f);
            alphaAnimation.setDuration(EXIT_ANIMATION_DURATION_MS);
            mBackground.startAnimation(alphaAnimation);
            mBackground.setVisibility(View.VISIBLE);

            final Animation scaleAnimation = new ScaleAnimation(1f, 1f, scale, scale);
            scaleAnimation.setDuration(EXIT_ANIMATION_DURATION_MS);
            AnimationListener listener = new AnimationListener() {
                @Override
                public void onAnimationEnd(Animation arg0) {
                    PhotoViewController.this.onExitAnimationComplete();
                }

                @Override
                public void onAnimationRepeat(Animation arg0) {
                }

                @Override
                public void onAnimationStart(Animation arg0) {
                }
            };
            scaleAnimation.setAnimationListener(listener);
            // If the temporary image is still visible it means that we have
            // not yet loaded the fullres image, so we need to animate
            // the temporary image out.
            if (mTemporaryImage.getVisibility() == View.VISIBLE) {
                mTemporaryImage.startAnimation(scaleAnimation);
            } else {
                mViewPager.startAnimation(scaleAnimation);
            }
        }
    
protected final voidsetActionBarTitles(ActionBarInterface actionBar)
Sets the Action Bar title to {@link #mActionBarTitle} and the subtitle to {@link #mActionBarSubtitle}

        if (actionBar == null) {
            return;
        }
        actionBar.setTitle(getInputOrEmpty(mActionBarTitle));
        actionBar.setSubtitle(getInputOrEmpty(mActionBarSubtitle));
    
protected voidsetFullScreen(boolean fullScreen, boolean setDelayedRunnable)
Updates the title bar according to the value of {@link #mFullScreen}.

        if (Util.isTouchExplorationEnabled(mAccessibilityManager)) {
            // Disallow full screen mode when accessibility is enabled so that the action bar
            // stays accessible.
            fullScreen = false;
            setDelayedRunnable = false;
        }

        final boolean fullScreenChanged = (fullScreen != mFullScreen);
        mFullScreen = fullScreen;

        if (mFullScreen) {
            setLightsOutMode(true);
            cancelEnterFullScreenRunnable();
        } else {
            setLightsOutMode(false);
            if (setDelayedRunnable) {
                postEnterFullScreenRunnableWithDelay();
            }
        }

        if (fullScreenChanged) {
            for (OnScreenListener listener : mScreenListeners.values()) {
                listener.onFullScreenChanged(mFullScreen);
            }
        }
    
public voidsetImmersiveMode(boolean enabled)

        int flags = 0;
        final int version = Build.VERSION.SDK_INT;
        final boolean manuallyUpdateActionBar = version < Build.VERSION_CODES.JELLY_BEAN;
        if (enabled &&
                (!isScaleAnimationEnabled() || isEnterAnimationFinished())) {
            // Turning on immersive mode causes an animation. If the scale animation is enabled and
            // the enter animation isn't yet complete, then an immersive mode animation should not
            // occur, since two concurrent animations are very janky.

            // Disable immersive mode for seconary users to prevent b/12015090 (freezing crash)
            // This is fixed in KK_MR2 but there is no way to differentiate between  KK and KK_MR2.
            if (version > Build.VERSION_CODES.KITKAT ||
                    version == Build.VERSION_CODES.KITKAT && !kitkatIsSecondaryUser()) {
                flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_IMMERSIVE;
            } else if (version >= Build.VERSION_CODES.JELLY_BEAN) {
                // Clients that use the scale animation should set the following system UI flags to
                // prevent janky animations on exit when the status bar is hidden:
                //     View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_STABLE
                // As well, client should ensure `android:fitsSystemWindows` is set on the root
                // content view.
                flags = View.SYSTEM_UI_FLAG_LOW_PROFILE
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_FULLSCREEN;
            } else if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                flags = View.SYSTEM_UI_FLAG_LOW_PROFILE;
            } else if (version >= Build.VERSION_CODES.HONEYCOMB) {
                flags = View.STATUS_BAR_HIDDEN;
            }

            if (manuallyUpdateActionBar) {
                hideActionBar();
            }
        } else {
            if (version >= Build.VERSION_CODES.KITKAT) {
                flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
            } else if (version >= Build.VERSION_CODES.JELLY_BEAN) {
                flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
            } else if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                flags = View.SYSTEM_UI_FLAG_VISIBLE;
            } else if (version >= Build.VERSION_CODES.HONEYCOMB) {
                flags = View.STATUS_BAR_VISIBLE;
            }

            if (manuallyUpdateActionBar) {
                showActionBar();
            }
        }

        if (version >= Build.VERSION_CODES.HONEYCOMB) {
            mLastFlags = flags;
            getRootView().setSystemUiVisibility(flags);
        }
    
protected voidsetLightsOutMode(boolean enabled)

        setImmersiveMode(enabled);
    
protected voidsetPhotoIndex(int index)

        mCurrentPhotoIndex = index;
    
public voidsetViewActivated(int position)


    
        
        OnScreenListener listener = mScreenListeners.get(position);
        if (listener != null) {
            listener.onViewActivated();
        }
        final Cursor cursor = getCursorAtProperPosition();
        mCurrentPhotoIndex = position;
        // FLAG: get the column indexes once in onLoadFinished().
        // That would make this more efficient, instead of looking these up
        // repeatedly whenever we want them.
        int uriIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
        mCurrentPhotoUri = cursor.getString(uriIndex);
        updateActionBar();
        if (mAccessibilityManager.isEnabled()) {
            String announcement = getPhotoAccessibilityAnnouncement(position);
            if (announcement != null) {
                Util.announceForAccessibility(mRootView, mAccessibilityManager, announcement);
            }
        }

        // Restart the timer to return to fullscreen.
        cancelEnterFullScreenRunnable();
        postEnterFullScreenRunnableWithDelay();
    
public voidshowActionBar()

        mActivity.getActionBarInterface().show();
    
public voidtoggleFullScreen()

        setFullScreen(!mFullScreen, true);
    
public voidupdateActionBar()
Adjusts the activity title and subtitle to reflect the photo name and count.

        final int position = mViewPager.getCurrentItem() + 1;
        final boolean hasAlbumCount = mAlbumCount >= 0;

        final Cursor cursor = getCursorAtProperPosition();
        if (cursor != null) {
            // FLAG: We should grab the indexes when we first get the cursor
            // and store them so we don't need to do it each time.
            final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME);
            mActionBarTitle = cursor.getString(photoNameIndex);
        } else {
            mActionBarTitle = null;
        }

        if (mIsEmpty || !hasAlbumCount || position <= 0) {
            mActionBarSubtitle = null;
        } else {
            mActionBarSubtitle = mActivity.getResources().getString(
                    R.string.photo_view_count, position, mAlbumCount);
        }

        setActionBarTitles(mActivity.getActionBarInterface());
    
public voidupdateActionItems()

        // Do nothing, but allow extending classes to do work