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