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