PhotoViewActivity.java revision 40a26567ebaf6311bd7a102491d669abd1d68646
1/* 2 * Copyright (C) 2011 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.ex.photo; 19 20import android.app.ActionBar; 21import android.app.ActionBar.OnMenuVisibilityListener; 22import android.app.Activity; 23import android.app.ActivityManager; 24import android.content.Context; 25import android.content.Intent; 26import android.content.res.Resources; 27import android.database.Cursor; 28import android.net.Uri; 29import android.os.Build; 30import android.os.Bundle; 31import android.os.Handler; 32import android.support.v4.app.Fragment; 33import android.support.v4.app.FragmentActivity; 34import android.support.v4.app.LoaderManager; 35import android.support.v4.content.Loader; 36import android.support.v4.view.ViewPager.OnPageChangeListener; 37import android.text.TextUtils; 38import android.view.MenuItem; 39import android.view.View; 40 41import com.android.ex.photo.PhotoViewPager.InterceptType; 42import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener; 43import com.android.ex.photo.adapters.PhotoPagerAdapter; 44import com.android.ex.photo.fragments.PhotoViewFragment; 45import com.android.ex.photo.loaders.PhotoPagerLoader; 46import com.android.ex.photo.provider.PhotoContract; 47 48import java.util.HashSet; 49import java.util.Set; 50 51/** 52 * Activity to view the contents of an album. 53 */ 54public class PhotoViewActivity extends FragmentActivity implements 55 LoaderManager.LoaderCallbacks<Cursor>, OnPageChangeListener, OnInterceptTouchListener, 56 OnMenuVisibilityListener, PhotoViewCallbacks { 57 58 private final static String STATE_ITEM_KEY = 59 "com.google.android.apps.plus.PhotoViewFragment.ITEM"; 60 private final static String STATE_FULLSCREEN_KEY = 61 "com.google.android.apps.plus.PhotoViewFragment.FULLSCREEN"; 62 63 private static final int LOADER_PHOTO_LIST = 1; 64 65 /** Count used when the real photo count is unknown [but, may be determined] */ 66 public static final int ALBUM_COUNT_UNKNOWN = -1; 67 68 /** Argument key for the dialog message */ 69 public static final String KEY_MESSAGE = "dialog_message"; 70 71 public static int sMemoryClass; 72 73 /** The URI of the photos we're viewing; may be {@code null} */ 74 private String mPhotosUri; 75 /** The URI of the initial photo to display */ 76 private String mInitialPhotoUri; 77 /** The index of the currently viewed photo */ 78 private int mPhotoIndex; 79 /** The query projection to use; may be {@code null} */ 80 private String[] mProjection; 81 /** The total number of photos; only valid if {@link #mIsEmpty} is {@code false}. */ 82 private int mAlbumCount = ALBUM_COUNT_UNKNOWN; 83 /** {@code true} if the view is empty. Otherwise, {@code false}. */ 84 private boolean mIsEmpty; 85 /** the main root view */ 86 protected View mRootView; 87 /** The main pager; provides left/right swipe between photos */ 88 protected PhotoViewPager mViewPager; 89 /** Adapter to create pager views */ 90 protected PhotoPagerAdapter mAdapter; 91 /** Whether or not we're in "full screen" mode */ 92 private boolean mFullScreen; 93 /** The set of listeners wanting full screen state */ 94 private Set<OnScreenListener> mScreenListeners = new HashSet<OnScreenListener>(); 95 /** The set of listeners wanting full screen state */ 96 private Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>(); 97 /** When {@code true}, restart the loader when the activity becomes active */ 98 private boolean mRestartLoader; 99 /** Whether or not this activity is paused */ 100 private boolean mIsPaused = true; 101 /** The maximum scale factor applied to images when they are initially displayed */ 102 private float mMaxInitialScale; 103 private final Handler mHandler = new Handler(); 104 // TODO Find a better way to do this. We basically want the activity to display the 105 // "loading..." progress until the fragment takes over and shows it's own "loading..." 106 // progress [located in photo_header_view.xml]. We could potentially have all status displayed 107 // by the activity, but, that gets tricky when it comes to screen rotation. For now, we 108 // track the loading by this variable which is fragile and may cause phantom "loading..." 109 // text. 110 private long mEnterFullScreenDelayTime; 111 112 protected PhotoPagerAdapter createPhotoPagerAdapter(Context context, 113 android.support.v4.app.FragmentManager fm, Cursor c, float maxScale) { 114 return new PhotoPagerAdapter(context, fm, c, maxScale); 115 } 116 117 @Override 118 protected void onCreate(Bundle savedInstanceState) { 119 super.onCreate(savedInstanceState); 120 121 final ActivityManager mgr = (ActivityManager) getApplicationContext(). 122 getSystemService(Activity.ACTIVITY_SERVICE); 123 sMemoryClass = mgr.getMemoryClass(); 124 125 Intent mIntent = getIntent(); 126 127 int currentItem = -1; 128 if (savedInstanceState != null) { 129 currentItem = savedInstanceState.getInt(STATE_ITEM_KEY, -1); 130 mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false); 131 } 132 133 // uri of the photos to view; optional 134 if (mIntent.hasExtra(Intents.EXTRA_PHOTOS_URI)) { 135 mPhotosUri = mIntent.getStringExtra(Intents.EXTRA_PHOTOS_URI); 136 } 137 138 // projection for the query; optional 139 // I.f not set, the default projection is used. 140 // This projection must include the columns from the default projection. 141 if (mIntent.hasExtra(Intents.EXTRA_PROJECTION)) { 142 mProjection = mIntent.getStringArrayExtra(Intents.EXTRA_PROJECTION); 143 } else { 144 mProjection = null; 145 } 146 147 // Set the current item from the intent if wasn't in the saved instance 148 if (mIntent.hasExtra(Intents.EXTRA_PHOTO_INDEX) && currentItem < 0) { 149 currentItem = mIntent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1); 150 } 151 if (mIntent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI) && currentItem < 0) { 152 mInitialPhotoUri = mIntent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI); 153 } 154 155 // Set the max initial scale, defaulting to 1x 156 mMaxInitialScale = mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f); 157 158 mPhotoIndex = currentItem; 159 160 setContentView(R.layout.photo_activity_view); 161 162 // Create the adapter and add the view pager 163 mAdapter = createPhotoPagerAdapter(this, getSupportFragmentManager(), 164 null, mMaxInitialScale); 165 final Resources resources = getResources(); 166 mRootView = findViewById(R.id.photo_activity_root_view); 167 mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager); 168 mViewPager.setOnPageChangeListener(this); 169 mViewPager.setOnInterceptTouchListener(this); 170 mViewPager.setPageMargin(resources.getDimensionPixelSize(R.dimen.photo_page_margin)); 171 172 // Kick off the loader 173 getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); 174 175 mEnterFullScreenDelayTime = 176 resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis); 177 178 final ActionBar actionBar = getActionBar(); 179 if (actionBar != null) { 180 actionBar.setDisplayHomeAsUpEnabled(true); 181 actionBar.addOnMenuVisibilityListener(this); 182 actionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE); 183 } 184 } 185 186 @Override 187 protected void onResume() { 188 super.onResume(); 189 setFullScreen(mFullScreen, false); 190 191 mIsPaused = false; 192 if (mRestartLoader) { 193 mRestartLoader = false; 194 getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this); 195 } 196 } 197 198 @Override 199 protected void onPause() { 200 mIsPaused = true; 201 202 super.onPause(); 203 } 204 205 @Override 206 public void onBackPressed() { 207 // If in full screen mode, toggle mode & eat the 'back' 208 if (mFullScreen) { 209 toggleFullScreen(); 210 } else { 211 super.onBackPressed(); 212 } 213 } 214 215 @Override 216 public void onSaveInstanceState(Bundle outState) { 217 super.onSaveInstanceState(outState); 218 219 outState.putInt(STATE_ITEM_KEY, mViewPager.getCurrentItem()); 220 outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen); 221 } 222 223 @Override 224 public boolean onOptionsItemSelected(MenuItem item) { 225 switch (item.getItemId()) { 226 case android.R.id.home: 227 finish(); 228 default: 229 return super.onOptionsItemSelected(item); 230 } 231 } 232 233 @Override 234 public void addScreenListener(OnScreenListener listener) { 235 mScreenListeners.add(listener); 236 } 237 238 @Override 239 public void removeScreenListener(OnScreenListener listener) { 240 mScreenListeners.remove(listener); 241 } 242 243 @Override 244 public synchronized void addCursorListener(CursorChangedListener listener) { 245 mCursorListeners.add(listener); 246 } 247 248 @Override 249 public synchronized void removeCursorListener(CursorChangedListener listener) { 250 mCursorListeners.remove(listener); 251 } 252 253 @Override 254 public boolean isFragmentFullScreen(Fragment fragment) { 255 if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) { 256 return mFullScreen; 257 } 258 return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment)); 259 } 260 261 @Override 262 public void toggleFullScreen() { 263 setFullScreen(!mFullScreen, true); 264 } 265 266 public void onPhotoRemoved(long photoId) { 267 final Cursor data = mAdapter.getCursor(); 268 if (data == null) { 269 // Huh?! How would this happen? 270 return; 271 } 272 273 final int dataCount = data.getCount(); 274 if (dataCount <= 1) { 275 finish(); 276 return; 277 } 278 279 getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this); 280 } 281 282 @Override 283 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 284 if (id == LOADER_PHOTO_LIST) { 285 return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mProjection); 286 } 287 return null; 288 } 289 290 @Override 291 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 292 final int id = loader.getId(); 293 if (id == LOADER_PHOTO_LIST) { 294 if (data == null || data.getCount() == 0) { 295 mIsEmpty = true; 296 } else { 297 mAlbumCount = data.getCount(); 298 299 if (mInitialPhotoUri != null) { 300 int index = 0; 301 int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI); 302 while (data.moveToNext()) { 303 String uri = data.getString(uriIndex); 304 if (TextUtils.equals(uri, mInitialPhotoUri)) { 305 mInitialPhotoUri = null; 306 mPhotoIndex = index; 307 break; 308 } 309 index++; 310 } 311 } 312 313 // We're paused; don't do anything now, we'll get re-invoked 314 // when the activity becomes active again 315 // TODO(pwestbro): This shouldn't be necessary, as the loader manager should 316 // restart the loader 317 if (mIsPaused) { 318 mRestartLoader = true; 319 return; 320 } 321 mIsEmpty = false; 322 323 mAdapter.swapCursor(data); 324 if (mViewPager.getAdapter() == null) { 325 mViewPager.setAdapter(mAdapter); 326 } 327 notifyCursorListeners(data); 328 329 // set the selected photo 330 int itemIndex = mPhotoIndex; 331 332 // Use an index of 0 if the index wasn't specified or couldn't be found 333 if (itemIndex < 0) { 334 itemIndex = 0; 335 } 336 337 mViewPager.setCurrentItem(itemIndex, false); 338 setViewActivated(); 339 } 340 // Update the any action items 341 updateActionItems(); 342 } 343 } 344 345 @Override 346 public void onLoaderReset(android.support.v4.content.Loader<Cursor> loader) { 347 // If the loader is reset, remove the reference in the adapter to this cursor 348 // TODO(pwestbro): reenable this when b/7075236 is fixed 349 // mAdapter.swapCursor(null); 350 } 351 352 protected void updateActionItems() { 353 // Do nothing, but allow extending classes to do work 354 } 355 356 private synchronized void notifyCursorListeners(Cursor data) { 357 // tell all of the objects listening for cursor changes 358 // that the cursor has changed 359 for (CursorChangedListener listener : mCursorListeners) { 360 listener.onCursorChanged(data); 361 } 362 } 363 364 @Override 365 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 366 } 367 368 @Override 369 public void onPageSelected(int position) { 370 mPhotoIndex = position; 371 setViewActivated(); 372 } 373 374 @Override 375 public void onPageScrollStateChanged(int state) { 376 } 377 378 @Override 379 public boolean isFragmentActive(Fragment fragment) { 380 if (mViewPager == null || mAdapter == null) { 381 return false; 382 } 383 return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment); 384 } 385 386 @Override 387 public void onFragmentVisible(PhotoViewFragment fragment) { 388 updateActionBar(fragment); 389 } 390 391 @Override 392 public InterceptType onTouchIntercept(float origX, float origY) { 393 boolean interceptLeft = false; 394 boolean interceptRight = false; 395 396 for (OnScreenListener listener : mScreenListeners) { 397 if (!interceptLeft) { 398 interceptLeft = listener.onInterceptMoveLeft(origX, origY); 399 } 400 if (!interceptRight) { 401 interceptRight = listener.onInterceptMoveRight(origX, origY); 402 } 403 listener.onViewActivated(); 404 } 405 406 if (interceptLeft) { 407 if (interceptRight) { 408 return InterceptType.BOTH; 409 } 410 return InterceptType.LEFT; 411 } else if (interceptRight) { 412 return InterceptType.RIGHT; 413 } 414 return InterceptType.NONE; 415 } 416 417 /** 418 * Updates the title bar according to the value of {@link #mFullScreen}. 419 */ 420 protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) { 421 final boolean fullScreenChanged = (fullScreen != mFullScreen); 422 mFullScreen = fullScreen; 423 424 if (mFullScreen) { 425 setLightsOutMode(true); 426 cancelEnterFullScreenRunnable(); 427 } else { 428 setLightsOutMode(false); 429 if (setDelayedRunnable) { 430 postEnterFullScreenRunnableWithDelay(); 431 } 432 } 433 434 if (fullScreenChanged) { 435 for (OnScreenListener listener : mScreenListeners) { 436 listener.onFullScreenChanged(mFullScreen); 437 } 438 } 439 } 440 441 private void postEnterFullScreenRunnableWithDelay() { 442 mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime); 443 } 444 445 private void cancelEnterFullScreenRunnable() { 446 mHandler.removeCallbacks(mEnterFullScreenRunnable); 447 } 448 449 protected void setLightsOutMode(boolean enabled) { 450 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 451 int flags = enabled 452 ? View.SYSTEM_UI_FLAG_LOW_PROFILE 453 | View.SYSTEM_UI_FLAG_FULLSCREEN 454 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 455 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 456 : View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 457 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 458 459 // using mViewPager since we have it and we need a view 460 mViewPager.setSystemUiVisibility(flags); 461 } else { 462 final ActionBar actionBar = getActionBar(); 463 if (enabled) { 464 actionBar.hide(); 465 } else { 466 actionBar.show(); 467 } 468 int flags = enabled 469 ? View.SYSTEM_UI_FLAG_LOW_PROFILE 470 : View.SYSTEM_UI_FLAG_VISIBLE; 471 mViewPager.setSystemUiVisibility(flags); 472 } 473 } 474 475 private Runnable mEnterFullScreenRunnable = new Runnable() { 476 @Override 477 public void run() { 478 setFullScreen(true, true); 479 } 480 }; 481 482 @Override 483 public void setViewActivated() { 484 for (OnScreenListener listener : mScreenListeners) { 485 listener.onViewActivated(); 486 } 487 } 488 489 /** 490 * Adjusts the activity title and subtitle to reflect the photo name and count. 491 */ 492 protected void updateActionBar(PhotoViewFragment fragment) { 493 final int position = mViewPager.getCurrentItem() + 1; 494 final String title; 495 final String subtitle; 496 final boolean hasAlbumCount = mAlbumCount >= 0; 497 498 final Cursor cursor = getCursorAtProperPosition(); 499 500 if (cursor != null) { 501 final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME); 502 title = cursor.getString(photoNameIndex); 503 } else { 504 title = null; 505 } 506 507 if (mIsEmpty || !hasAlbumCount || position <= 0) { 508 subtitle = null; 509 } else { 510 subtitle = getResources().getString(R.string.photo_view_count, position, mAlbumCount); 511 } 512 513 final ActionBar actionBar = getActionBar(); 514 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE); 515 actionBar.setTitle(title); 516 actionBar.setSubtitle(subtitle); 517 } 518 519 /** 520 * Utility method that will return the cursor that contains the data 521 * at the current position so that it refers to the current image on screen. 522 * @return the cursor at the current position or 523 * null if no cursor exists or if the {@link PhotoViewPager} is null. 524 */ 525 public Cursor getCursorAtProperPosition() { 526 if (mViewPager == null) { 527 return null; 528 } 529 530 final int position = mViewPager.getCurrentItem(); 531 final Cursor cursor = mAdapter.getCursor(); 532 533 if (cursor == null) { 534 return null; 535 } 536 537 cursor.moveToPosition(position); 538 539 return cursor; 540 } 541 542 public Cursor getCursor() { 543 return (mAdapter == null) ? null : mAdapter.getCursor(); 544 } 545 546 @Override 547 public void onMenuVisibilityChanged(boolean isVisible) { 548 if (isVisible) { 549 cancelEnterFullScreenRunnable(); 550 } else { 551 postEnterFullScreenRunnableWithDelay(); 552 } 553 } 554 555 public void onNewPhotoLoaded() { 556 setViewActivated(); 557 } 558 559 protected boolean isFullScreen() { 560 return mFullScreen; 561 } 562 563 protected void setPhotoIndex(int index) { 564 mPhotoIndex = index; 565 } 566 567} 568