1 package com.android.ex.photo;
2 
3 import android.app.Activity;
4 import android.app.ActivityManager;
5 import android.content.Context;
6 import android.content.Intent;
7 import android.content.res.Resources;
8 import android.database.Cursor;
9 import android.graphics.drawable.Drawable;
10 import android.net.Uri;
11 import android.os.Build;
12 import android.os.Bundle;
13 import android.os.Handler;
14 import android.os.Process;
15 import android.support.v4.app.Fragment;
16 import android.support.v4.app.FragmentManager;
17 import android.support.v4.app.LoaderManager;
18 import android.support.v4.content.Loader;
19 import android.support.v4.view.ViewPager.OnPageChangeListener;
20 import android.text.TextUtils;
21 import android.util.DisplayMetrics;
22 import android.util.Log;
23 import android.view.Menu;
24 import android.view.MenuItem;
25 import android.view.View;
26 import android.view.ViewPropertyAnimator;
27 import android.view.WindowManager;
28 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
29 import android.view.accessibility.AccessibilityManager;
30 import android.view.animation.AlphaAnimation;
31 import android.view.animation.Animation;
32 import android.view.animation.Animation.AnimationListener;
33 import android.view.animation.AnimationSet;
34 import android.view.animation.ScaleAnimation;
35 import android.view.animation.TranslateAnimation;
36 import android.widget.ImageView;
37 
38 import com.android.ex.photo.ActionBarInterface.OnMenuVisibilityListener;
39 import com.android.ex.photo.PhotoViewPager.InterceptType;
40 import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener;
41 import com.android.ex.photo.adapters.PhotoPagerAdapter;
42 import com.android.ex.photo.fragments.PhotoViewFragment;
43 import com.android.ex.photo.loaders.PhotoBitmapLoader;
44 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
45 import com.android.ex.photo.loaders.PhotoPagerLoader;
46 import com.android.ex.photo.provider.PhotoContract;
47 import com.android.ex.photo.util.ImageUtils;
48 import com.android.ex.photo.util.Util;
49 
50 import java.util.HashMap;
51 import java.util.HashSet;
52 import java.util.Map;
53 import 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  */
64 public 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 {
getContext()75         public Context getContext();
getApplicationContext()76         public Context getApplicationContext();
getIntent()77         public Intent getIntent();
setContentView(int resId)78         public void setContentView(int resId);
findViewById(int id)79         public View findViewById(int id);
getResources()80         public Resources getResources();
getSupportFragmentManager()81         public FragmentManager getSupportFragmentManager();
getSupportLoaderManager()82         public LoaderManager getSupportLoaderManager();
getActionBarInterface()83         public ActionBarInterface getActionBarInterface();
onOptionsItemSelected(MenuItem item)84         public boolean onOptionsItemSelected(MenuItem item);
finish()85         public void finish();
overridePendingTransition(int enterAnim, int exitAnim)86         public void overridePendingTransition(int enterAnim, int exitAnim);
getController()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 
PhotoViewController(ActivityInterface activity)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 
createPhotoPagerAdapter(Context context, android.support.v4.app.FragmentManager fm, Cursor c, float maxScale)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 
getActivity()225     public PhotoViewController.ActivityInterface getActivity() {
226         return mActivity;
227     }
228 
onCreate(Bundle savedInstanceState)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 
initMaxPhotoSize()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 
onCreateOptionsMenu(Menu menu)378     public boolean onCreateOptionsMenu(Menu menu) {
379         return true;
380     }
381 
onPrepareOptionsMenu(Menu menu)382     public boolean onPrepareOptionsMenu(Menu menu) {
383         return true;
384     }
385 
onActivityResult(int requestCode, int resultCode, Intent data)386     public void onActivityResult(int requestCode, int resultCode, Intent data) {}
387 
findViewById(int id)388     protected View findViewById(int id) {
389         return mActivity.findViewById(id);
390     }
391 
onStart()392     public void onStart() {}
393 
onResume()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 
onPause()404     public void onPause() {
405         mIsPaused = true;
406     }
407 
onStop()408     public void onStop() {}
409 
onDestroy()410     public void onDestroy() {
411         mIsDestroyedCompat = true;
412     }
413 
isDestroyedCompat()414     private boolean isDestroyedCompat() {
415         return mIsDestroyedCompat;
416     }
417 
onBackPressed()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 
onSaveInstanceState(Bundle outState)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 
onOptionsItemSelected(MenuItem item)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
addScreenListener(int position, OnScreenListener listener)454     public void addScreenListener(int position, OnScreenListener listener) {
455         mScreenListeners.put(position, listener);
456     }
457 
458     @Override
removeScreenListener(int position)459     public void removeScreenListener(int position) {
460         mScreenListeners.remove(position);
461     }
462 
463     @Override
addCursorListener(CursorChangedListener listener)464     public synchronized void addCursorListener(CursorChangedListener listener) {
465         mCursorListeners.add(listener);
466     }
467 
468     @Override
removeCursorListener(CursorChangedListener listener)469     public synchronized void removeCursorListener(CursorChangedListener listener) {
470         mCursorListeners.remove(listener);
471     }
472 
473     @Override
isFragmentFullScreen(Fragment fragment)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
toggleFullScreen()482     public void toggleFullScreen() {
483         setFullScreen(!mFullScreen, true);
484     }
485 
onPhotoRemoved(long photoId)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
onCreateLoader(int id, Bundle args)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
onCreateBitmapLoader(int id, Bundle args, String uri)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
onLoadFinished(Loader<Cursor> loader, Cursor data)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
onLoaderReset(android.support.v4.content.Loader<Cursor> loader)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 
updateActionItems()602     public void updateActionItems() {
603         // Do nothing, but allow extending classes to do work
604     }
605 
notifyCursorListeners(Cursor data)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
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)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
onPageSelected(int position)629     public void onPageSelected(int position) {
630         mCurrentPhotoIndex = position;
631         setViewActivated(position);
632     }
633 
634     @Override
onPageScrollStateChanged(int state)635     public void onPageScrollStateChanged(int state) {
636     }
637 
638     @Override
isFragmentActive(Fragment fragment)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
onFragmentVisible(PhotoViewFragment fragment)647     public void onFragmentVisible(PhotoViewFragment fragment) {
648         // Do nothing, we handle this in setViewActivated
649     }
650 
651     @Override
onTouchIntercept(float origX, float origY)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      */
setFullScreen(boolean fullScreen, boolean setDelayedRunnable)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 
postEnterFullScreenRunnableWithDelay()707     private void postEnterFullScreenRunnableWithDelay() {
708         mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime);
709     }
710 
cancelEnterFullScreenRunnable()711     private void cancelEnterFullScreenRunnable() {
712         mHandler.removeCallbacks(mEnterFullScreenRunnable);
713     }
714 
setLightsOutMode(boolean enabled)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
setViewActivated(int position)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      */
updateActionBar()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      */
getPhotoAccessibilityAnnouncement(int position)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      */
setActionBarTitles(ActionBarInterface actionBar)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      */
getInputOrEmpty(String in)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      */
getCursorAtProperPosition()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 
getCursor()841     public Cursor getCursor() {
842         return (mAdapter == null) ? null : mAdapter.getCursor();
843     }
844 
845     @Override
onMenuVisibilityChanged(boolean isVisible)846     public void onMenuVisibilityChanged(boolean isVisible) {
847         if (isVisible) {
848             cancelEnterFullScreenRunnable();
849         } else {
850             postEnterFullScreenRunnableWithDelay();
851         }
852     }
853 
854     @Override
onNewPhotoLoaded(int position)855     public void onNewPhotoLoaded(int position) {
856         // do nothing
857     }
858 
setPhotoIndex(int index)859     protected void setPhotoIndex(int index) {
860         mCurrentPhotoIndex = index;
861     }
862 
863     @Override
onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success)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 
isFullScreen()885     protected boolean isFullScreen() {
886         return mFullScreen;
887     }
888 
889     @Override
onCursorChanged(PhotoViewFragment fragment, Cursor cursor)890     public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) {
891         // do nothing
892     }
893 
894     @Override
getAdapter()895     public PhotoPagerAdapter getAdapter() {
896         return mAdapter;
897     }
898 
onEnterAnimationComplete()899     public void onEnterAnimationComplete() {
900         mEnterAnimationFinished = true;
901         mViewPager.setVisibility(View.VISIBLE);
902         setLightsOutMode(mFullScreen);
903     }
904 
onExitAnimationComplete()905     private void onExitAnimationComplete() {
906         mActivity.finish();
907         mActivity.overridePendingTransition(0, 0);
908     }
909 
runEnterAnimation()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 
runExitAnimation()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 
calculateTranslate(int start, int startSize, int totalSize, float scale)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 
initTemporaryImage(Drawable drawable)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 
showActionBar()1143     public void showActionBar() {
1144         mActivity.getActionBarInterface().show();
1145     }
1146 
hideActionBar()1147     public void hideActionBar() {
1148         mActivity.getActionBarInterface().hide();
1149     }
1150 
isScaleAnimationEnabled()1151     public boolean isScaleAnimationEnabled() {
1152         return mScaleAnimationEnabled;
1153     }
1154 
isEnterAnimationFinished()1155     public boolean isEnterAnimationFinished() {
1156         return mEnterAnimationFinished;
1157     }
1158 
getRootView()1159     public View getRootView() {
1160         return mRootView;
1161     }
1162 
1163     private class BitmapCallback implements LoaderManager.LoaderCallbacks<BitmapResult> {
1164 
1165         @Override
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<BitmapResult> loader, BitmapResult result)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
onLoaderReset(Loader<BitmapResult> loader)1200         public void onLoaderReset(Loader<BitmapResult> loader) {
1201             // Do nothing
1202         }
1203     }
1204 
setImmersiveMode(boolean enabled)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