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