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