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