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