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