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