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