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