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