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