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