1package com.android.ex.photo;
2
3import android.app.Activity;
4import android.app.ActivityManager;
5import android.content.Context;
6import android.content.Intent;
7import android.content.res.Resources;
8import android.database.Cursor;
9import android.graphics.drawable.Drawable;
10import android.net.Uri;
11import android.os.Build;
12import android.os.Bundle;
13import android.os.Handler;
14import android.os.Process;
15import android.support.annotation.IdRes;
16import android.support.annotation.Nullable;
17import android.support.v4.app.Fragment;
18import android.support.v4.app.FragmentManager;
19import android.support.v4.app.LoaderManager;
20import android.support.v4.content.Loader;
21import android.support.v4.view.ViewPager.OnPageChangeListener;
22import android.text.TextUtils;
23import android.util.DisplayMetrics;
24import android.util.Log;
25import android.view.Menu;
26import android.view.MenuItem;
27import android.view.View;
28import android.view.ViewPropertyAnimator;
29import android.view.ViewTreeObserver.OnGlobalLayoutListener;
30import android.view.WindowManager;
31import android.view.accessibility.AccessibilityManager;
32import android.view.animation.AlphaAnimation;
33import android.view.animation.Animation;
34import android.view.animation.Animation.AnimationListener;
35import android.view.animation.AnimationSet;
36import android.view.animation.ScaleAnimation;
37import android.view.animation.TranslateAnimation;
38import android.widget.ImageView;
39
40import com.android.ex.photo.ActionBarInterface.OnMenuVisibilityListener;
41import com.android.ex.photo.PhotoViewPager.InterceptType;
42import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener;
43import com.android.ex.photo.adapters.PhotoPagerAdapter;
44import com.android.ex.photo.fragments.PhotoViewFragment;
45import com.android.ex.photo.loaders.PhotoBitmapLoader;
46import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
47import com.android.ex.photo.loaders.PhotoPagerLoader;
48import com.android.ex.photo.provider.PhotoContract;
49import com.android.ex.photo.util.ImageUtils;
50import com.android.ex.photo.util.Util;
51
52import java.util.HashMap;
53import java.util.HashSet;
54import java.util.Map;
55import 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 */
66public 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