| /* |
| * Copyright (C) 2011 Google Inc. |
| * Licensed to The Android Open Source Project. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.mail.photo; |
| |
| import android.app.ActionBar; |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.app.AlertDialog; |
| import android.app.Dialog; |
| import android.app.ProgressDialog; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.support.v4.app.Fragment; |
| import android.support.v4.app.FragmentActivity; |
| import android.support.v4.app.LoaderManager.LoaderCallbacks; |
| import android.support.v4.content.Loader; |
| import android.support.v4.view.ViewPager.OnPageChangeListener; |
| import android.view.View; |
| import android.widget.ProgressBar; |
| import android.widget.TextView; |
| |
| import com.android.mail.R; |
| import com.android.mail.photo.PhotoViewPager.InterceptType; |
| import com.android.mail.photo.PhotoViewPager.OnInterceptTouchListener; |
| import com.android.mail.photo.adapters.BaseFragmentPagerAdapter.OnFragmentPagerListener; |
| import com.android.mail.photo.adapters.PhotoPagerAdapter; |
| import com.android.mail.photo.fragments.PhotoViewFragment; |
| import com.android.mail.photo.fragments.PhotoViewFragment.PhotoViewCallbacks; |
| import com.android.mail.photo.loaders.PhotoCursorLoader; |
| import com.android.mail.photo.loaders.PhotoPagerLoader; |
| import com.android.mail.photo.provider.PhotoContract; |
| |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| /** |
| * Activity to view the contents of an album. |
| */ |
| public class PhotoViewActivity extends FragmentActivity implements PhotoViewCallbacks, |
| LoaderCallbacks<Cursor>, OnPageChangeListener, OnInterceptTouchListener, |
| OnFragmentPagerListener { |
| |
| /** |
| * Listener to be invoked for screen events. |
| */ |
| public static interface OnScreenListener { |
| |
| /** |
| * The full screen state has changed. |
| */ |
| public void onFullScreenChanged(boolean fullScreen, boolean animate); |
| |
| /** |
| * A new view has been activated and the previous view de-activated. |
| */ |
| public void onViewActivated(); |
| |
| /** |
| * Updates the view that can be used to show progress. |
| * |
| * @param progressView a View that can be used to show progress |
| */ |
| public void onUpdateProgressView(ProgressBar progressView); |
| |
| /** |
| * Called when a right-to-left touch move intercept is about to occur. |
| * |
| * @param origX the raw x coordinate of the initial touch |
| * @param origY the raw y coordinate of the initial touch |
| * @return {@code true} if the touch should be intercepted. |
| */ |
| public boolean onInterceptMoveLeft(float origX, float origY); |
| |
| /** |
| * Called when a left-to-right touch move intercept is about to occur. |
| * |
| * @param origX the raw x coordinate of the initial touch |
| * @param origY the raw y coordinate of the initial touch |
| * @return {@code true} if the touch should be intercepted. |
| */ |
| public boolean onInterceptMoveRight(float origX, float origY); |
| } |
| |
| private final static String STATE_ITEM_KEY = |
| "com.google.android.apps.plus.PhotoViewFragment.ITEM"; |
| private final static String STATE_FULLSCREEN_KEY = |
| "com.google.android.apps.plus.PhotoViewFragment.FULLSCREEN"; |
| |
| private static final int LOADER_PHOTO_LIST = R.id.photo_view_photo_list_loader_id; |
| |
| /** Count used when the real photo count is unknown [but, may be determined] */ |
| public static final int ALBUM_COUNT_UNKNOWN = -1; |
| /** Count used when the real photo count can't be know [eg for a photo stream] */ |
| public static final int ALBUM_COUNT_UNKNOWABLE = -2; |
| |
| /** Argument key for the dialog message */ |
| public static final String KEY_MESSAGE = "dialog_message"; |
| |
| public static int sMemoryClass; |
| |
| /** The URI of the photos we're viewing; may be {@code null} */ |
| private String mPhotosUri; |
| /** The index of the currently viewed photo */ |
| private int mPhotoIndex; |
| /** The query projection to use; may be {@code null} */ |
| private String[] mProjection; |
| /** A hint for which cursor page the photo is located on */ |
| private int mPageHint = PhotoCursorLoader.LOAD_LIMIT_UNLIMITED; |
| /** The name of the particular photo being viewed. */ |
| private String mPhotoName; |
| /** The total number of photos; only valid if {@link #mIsEmpty} is {@code false}. */ |
| private int mAlbumCount = ALBUM_COUNT_UNKNOWN; |
| /** {@code true} if the view is empty. Otherwise, {@code false}. */ |
| private boolean mIsEmpty; |
| /** The root view of the activity */ |
| private View mRootView; |
| /** The main pager; provides left/right swipe between photos */ |
| private PhotoViewPager mViewPager; |
| /** Adapter to create pager views */ |
| private PhotoPagerAdapter mAdapter; |
| /** Whether or not we're in "full screen" mode */ |
| private boolean mFullScreen; |
| /** Whether or not we should only show the photo and no extra information */ |
| private boolean mShowPhotoOnly; |
| /** The set of listeners wanting full screen state */ |
| private Set<OnScreenListener> mScreenListeners = new HashSet<OnScreenListener>(); |
| /** When {@code true}, restart the loader when the activity becomes active */ |
| private boolean mRestartLoader; |
| /** Whether or not this activity is paused */ |
| private boolean mIsPaused = true; |
| // TODO Find a better way to do this. We basically want the activity to display the |
| // "loading..." progress until the fragment takes over and shows it's own "loading..." |
| // progress [located in photo_header_view.xml]. We could potentially have all status displayed |
| // by the activity, but, that gets tricky when it comes to screen rotation. For now, we |
| // track the loading by this variable which is fragile and may cause phantom "loading..." |
| // text. |
| /** {@code true} if the fragment is loading. */ |
| private boolean mFragmentIsLoading; |
| |
| /** Listener to handle dialog button clicks for the failed dialog. */ |
| private DialogInterface.OnClickListener mFailedListener = |
| new DialogInterface.OnClickListener() { |
| |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| dialog.dismiss(); |
| } |
| }; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| final ActivityManager mgr = (ActivityManager) getApplicationContext(). |
| getSystemService(Activity.ACTIVITY_SERVICE); |
| sMemoryClass = mgr.getMemoryClass(); |
| |
| Intent mIntent = getIntent(); |
| mShowPhotoOnly = mIntent.getBooleanExtra(Intents.EXTRA_SHOW_PHOTO_ONLY, false); |
| |
| int currentItem = -1; |
| if (savedInstanceState != null) { |
| currentItem = savedInstanceState.getInt(STATE_ITEM_KEY, -1); |
| mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false); |
| } else { |
| mFullScreen = mShowPhotoOnly; |
| } |
| |
| // album name; if not set, use a default name |
| if (mIntent.hasExtra(Intents.EXTRA_PHOTO_NAME)) { |
| mPhotoName = mIntent.getStringExtra(Intents.EXTRA_PHOTO_NAME); |
| } else { |
| mPhotoName = getResources().getString(R.string.photo_view_default_title); |
| } |
| |
| // uri of the photos to view; optional |
| if (mIntent.hasExtra(Intents.EXTRA_PHOTOS_URI)) { |
| mPhotosUri = mIntent.getStringExtra(Intents.EXTRA_PHOTOS_URI); |
| } |
| |
| // projection for the query; optional |
| // I.f not set, the default projection is used. |
| // This projection must include the columns from the default projection. |
| if (mIntent.hasExtra(Intents.EXTRA_PROJECTION)) { |
| mProjection = mIntent.getStringArrayExtra(Intents.EXTRA_PROJECTION); |
| } else { |
| mProjection = null; |
| } |
| |
| // the loader page hint |
| if (mIntent.hasExtra(Intents.EXTRA_PAGE_HINT) && currentItem < 0) { |
| mPageHint = mIntent.getIntExtra(Intents.EXTRA_PAGE_HINT, |
| PhotoCursorLoader.LOAD_LIMIT_UNLIMITED); |
| } |
| // Set the current item from the intent if wasn't in the saved instance |
| if (mIntent.hasExtra(Intents.EXTRA_PHOTO_INDEX) && currentItem < 0) { |
| currentItem = mIntent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1); |
| } |
| mPhotoIndex = currentItem; |
| |
| setContentView(R.layout.photo_activity_view); |
| mRootView = findViewById(R.id.photo_activity_root_view); |
| // Create the adapter and add the view pager |
| final Long forceLoadId = (mIntent.hasExtra(Intents.EXTRA_REFRESH)) |
| ? mIntent.getLongExtra(Intents.EXTRA_REFRESH, 0L) |
| : null; |
| |
| mAdapter = new PhotoPagerAdapter(this, getSupportFragmentManager(), null, |
| forceLoadId); |
| mAdapter.setFragmentPagerListener(this); |
| |
| mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager); |
| mViewPager.setAdapter(mAdapter); |
| mViewPager.setOnPageChangeListener(this); |
| mViewPager.setOnInterceptTouchListener(this); |
| |
| // Kick off the loaders |
| getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); |
| |
| final ActionBar actionBar = getActionBar(); |
| actionBar.setDisplayHomeAsUpEnabled(true); |
| |
| updateView(mRootView); |
| } |
| |
| @Override |
| protected void onResume() { |
| super.onResume(); |
| setFullScreen(mFullScreen, false); |
| |
| mIsPaused = false; |
| if (mRestartLoader) { |
| mRestartLoader = false; |
| getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this); |
| } |
| } |
| |
| @Override |
| protected void onPause() { |
| mIsPaused = true; |
| |
| super.onPause(); |
| } |
| |
| @Override |
| public void onBackPressed() { |
| // If in full screen mode, toggle mode & eat the 'back' |
| if (mFullScreen && !mShowPhotoOnly) { |
| toggleFullScreen(); |
| } else { |
| super.onBackPressed(); |
| } |
| } |
| |
| @Override |
| public void onAttachFragment(Fragment fragment) { |
| super.onAttachFragment(fragment); |
| PhotoViewFragment photoFragment = null; |
| if (fragment instanceof PhotoViewFragment) { |
| photoFragment = (PhotoViewFragment) fragment; |
| } |
| |
| // Set the progress view as new fragments are attached |
| final ProgressBar progressView |
| = (ProgressBar) findViewById(R.id.action_bar_progress_spinner_view); |
| |
| if (photoFragment != null && progressView != null) { |
| photoFragment.onUpdateProgressView(progressView); |
| } |
| } |
| |
| @Override |
| protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { |
| super.onPrepareDialog(id, dialog, args); |
| if (id == R.id.photo_view_pending_dialog) { |
| // Update the message each time this dialog is shown in order |
| // to ensure it matches the current operation. |
| if (dialog instanceof ProgressDialog) { |
| // This should always be true |
| final ProgressDialog pd = (ProgressDialog) dialog; |
| pd.setMessage(args.getString(KEY_MESSAGE)); |
| } |
| } |
| } |
| |
| @Override |
| protected Dialog onCreateDialog(int id, Bundle args) { |
| String tag = args.getString(Intents.EXTRA_TAG); |
| if (id == R.id.photo_view_pending_dialog) { |
| final ProgressDialog progressDialog = new ProgressDialog(this); |
| progressDialog.setMessage(args.getString(KEY_MESSAGE)); |
| progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); |
| progressDialog.setCancelable(false); |
| return progressDialog; |
| } else if (id == R.id.photo_view_download_full_failed_dialog) { |
| final RetryDialogListener retryListener = new RetryDialogListener(tag); |
| final AlertDialog.Builder builder = new AlertDialog.Builder(this); |
| builder.setMessage(R.string.download_photo_retry) |
| .setPositiveButton(R.string.yes, retryListener) |
| .setNegativeButton(R.string.no, retryListener); |
| return builder.create(); |
| } else if (id == R.id.photo_view_download_nonfull_failed_dialog) { |
| final AlertDialog.Builder builder = new AlertDialog.Builder(this); |
| builder.setMessage(R.string.download_photo_error) |
| .setNeutralButton(R.string.ok, mFailedListener); |
| return builder.create(); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| |
| outState.putInt(STATE_ITEM_KEY, mViewPager.getCurrentItem()); |
| outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen); |
| } |
| |
| @Override |
| public void addScreenListener(OnScreenListener listener) { |
| mScreenListeners.add(listener); |
| } |
| |
| @Override |
| public void removeScreenListener(OnScreenListener listener) { |
| mScreenListeners.remove(listener); |
| } |
| |
| @Override |
| public boolean isFragmentFullScreen(Fragment fragment) { |
| if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) { |
| return mFullScreen; |
| } |
| return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment)); |
| } |
| |
| @Override |
| public boolean isShowPhotoOnly() { |
| return mShowPhotoOnly; |
| } |
| |
| @Override |
| public void toggleFullScreen() { |
| setFullScreen(!mFullScreen, true); |
| } |
| |
| @Override |
| public void onPhotoRemoved(long photoId) { |
| final Cursor data = mAdapter.getCursor(); |
| if (data == null) { |
| // Huh?! How would this happen? |
| return; |
| } |
| |
| final int dataCount = data.getCount(); |
| if (dataCount <= 1) { |
| // The last photo was removed ... finish the activity & go to photos-home |
| // final Intent intent = Intents.getPhotosHomeIntent(this, mAccount, mAccount.getGaiaId()); |
| // |
| // intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); |
| // startActivity(intent); |
| finish(); |
| return; |
| } |
| |
| getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this); |
| } |
| |
| @Override |
| public Loader<Cursor> onCreateLoader(int id, Bundle args) { |
| if (id == LOADER_PHOTO_LIST) { |
| mFragmentIsLoading = true; |
| return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mPageHint, mProjection); |
| } |
| return null; |
| } |
| |
| @Override |
| public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) { |
| final int id = loader.getId(); |
| if (id == LOADER_PHOTO_LIST) { |
| if (data == null || data.getCount() == 0) { |
| mIsEmpty = true; |
| mFragmentIsLoading = false; |
| updateView(mRootView); |
| } else { |
| mAlbumCount = data.getCount(); |
| |
| // Cannot do this directly; need to be out of the loader |
| new Handler().post(new Runnable() { |
| @Override |
| public void run() { |
| // We're paused; don't do anything now, we'll get re-invoked |
| // when the activity becomes active again |
| if (mIsPaused) { |
| mRestartLoader = true; |
| return; |
| } |
| mIsEmpty = false; |
| |
| // set the selected photo |
| int itemIndex = mPhotoIndex; |
| |
| // Use an index of 0 if the index wasn't specified or couldn't be found |
| if (itemIndex < 0) { |
| itemIndex = 0; |
| } |
| |
| mAdapter.setPageable((Pageable) loader); |
| mAdapter.swapCursor(data); |
| updateView(mRootView); |
| mViewPager.setCurrentItem(itemIndex, false); |
| updateActionBar(); |
| } |
| }); |
| } |
| } |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<Cursor> loader) { |
| } |
| |
| @Override |
| public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { |
| } |
| |
| @Override |
| public void onPageSelected(int position) { |
| setViewActivated(); |
| updateActionBar(); |
| mPhotoIndex = position; |
| } |
| |
| @Override |
| public void onPageScrollStateChanged(int state) { |
| } |
| |
| @Override |
| public void onPageActivated(Fragment fragment) { |
| setViewActivated(); |
| } |
| |
| @Override |
| public boolean isFragmentActive(Fragment fragment) { |
| if (mViewPager == null || mAdapter == null) { |
| return false; |
| } |
| return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment); |
| } |
| |
| @Override |
| public void onFragmentVisible(Fragment fragment) { |
| if (mViewPager == null || mAdapter == null) { |
| return; |
| } |
| if (mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment)) { |
| mFragmentIsLoading = false; |
| } |
| updateView(mRootView); |
| } |
| |
| @Override |
| public InterceptType onTouchIntercept(float origX, float origY) { |
| boolean interceptLeft = false; |
| boolean interceptRight = false; |
| |
| for (OnScreenListener listener : mScreenListeners) { |
| if (!interceptLeft) { |
| interceptLeft = listener.onInterceptMoveLeft(origX, origY); |
| } |
| if (!interceptRight) { |
| interceptRight = listener.onInterceptMoveRight(origX, origY); |
| } |
| listener.onViewActivated(); |
| } |
| |
| if (interceptLeft) { |
| if (interceptRight) { |
| return InterceptType.BOTH; |
| } |
| return InterceptType.LEFT; |
| } else if (interceptRight) { |
| return InterceptType.RIGHT; |
| } |
| return InterceptType.NONE; |
| } |
| |
| /** |
| * Updates the title bar according to the value of {@link #mFullScreen}. |
| */ |
| private void setFullScreen(boolean fullScreen, boolean animate) { |
| final boolean fullScreenChanged = (fullScreen != mFullScreen); |
| mFullScreen = fullScreen; |
| |
| ActionBar actionBar = getActionBar(); |
| if (mFullScreen) { |
| actionBar.hide(); |
| } else { |
| actionBar.show(); |
| } |
| |
| if (fullScreenChanged) { |
| for (OnScreenListener listener : mScreenListeners) { |
| listener.onFullScreenChanged(mFullScreen, animate); |
| } |
| } |
| } |
| |
| /** |
| * Updates the title bar according to the value of {@link #mFullScreen}. |
| */ |
| private void setViewActivated() { |
| for (OnScreenListener listener : mScreenListeners) { |
| listener.onViewActivated(); |
| } |
| } |
| |
| /** |
| * Updates the view to show the correct content. If album data is available, show the album |
| * list. Otherwise, show either progress or no album view. |
| */ |
| private void updateView(View view) { |
| if (view == null) { |
| return; |
| } |
| |
| if (mFragmentIsLoading || (mAdapter.getCursor() == null && !mIsEmpty)) { |
| showEmptyViewProgress(view); |
| } else { |
| if (!mIsEmpty) { |
| showContent(view); |
| } else { |
| showEmptyView(view, getResources().getString(R.string.camera_photo_error)); |
| } |
| } |
| } |
| |
| /** |
| * Display loading progress |
| * |
| * @param view The layout view |
| */ |
| private void showEmptyViewProgress(View view) { |
| view.findViewById(R.id.photo_activity_empty_text).setVisibility(View.GONE); |
| view.findViewById(R.id.photo_activity_empty_progress).setVisibility(View.VISIBLE); |
| view.findViewById(R.id.photo_activity_empty).setVisibility(View.VISIBLE); |
| } |
| |
| /** |
| * Show only the empty view |
| * |
| * @param view The layout view |
| */ |
| private void showEmptyView(View view, CharSequence emptyText) { |
| view.findViewById(R.id.photo_activity_empty_progress).setVisibility(View.GONE); |
| final TextView etv = (TextView) view.findViewById(R.id.photo_activity_empty_text); |
| etv.setText(emptyText); |
| etv.setVisibility(View.VISIBLE); |
| view.findViewById(R.id.photo_activity_empty).setVisibility(View.VISIBLE); |
| } |
| |
| /** |
| * Hide the empty view and show the content |
| * |
| * @param view The layout view |
| */ |
| private void showContent(View view) { |
| view.findViewById(R.id.photo_activity_empty).setVisibility(View.GONE); |
| } |
| |
| /** |
| * Adjusts the activity title and subtitle to reflect the photo name and count. |
| */ |
| protected void updateActionBar() { |
| final int position = mViewPager.getCurrentItem() + 1; |
| final String subtitle; |
| final boolean hasAlbumCount = mAlbumCount >= 0; |
| |
| final Cursor cursor = getCursorAtProperPosition(); |
| |
| if (cursor != null) { |
| final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME); |
| mPhotoName = cursor.getString(photoNameIndex); |
| } |
| |
| if (mIsEmpty || !hasAlbumCount || position <= 0) { |
| subtitle = null; |
| } else { |
| subtitle = getResources().getString(R.string.photo_view_count, position, mAlbumCount); |
| } |
| |
| final ActionBar actionBar = getActionBar(); |
| |
| actionBar.setTitle(mPhotoName); |
| actionBar.setSubtitle(subtitle); |
| } |
| |
| /** |
| * Utility method that will return the cursor that contains the data |
| * at the current position so that it refers to the current image on screen. |
| * @return the cursor at the current position or |
| * null if no cursor exists or if the {@link PhotoViewPager} is null. |
| */ |
| public Cursor getCursorAtProperPosition() { |
| if (mViewPager == null) { |
| return null; |
| } |
| |
| final int position = mViewPager.getCurrentItem(); |
| final Cursor cursor = mAdapter.getCursor(); |
| |
| if (cursor == null) { |
| return null; |
| } |
| |
| cursor.moveToPosition(position); |
| |
| return cursor; |
| } |
| |
| /** |
| * Listener to handle dialog button clicks for the retry dialog. |
| */ |
| class RetryDialogListener implements DialogInterface.OnClickListener { |
| /** The tag of the fragment this dialog is opened for */ |
| final String mTag; |
| |
| public RetryDialogListener(String tag) { |
| mTag = tag; |
| } |
| |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| switch (which) { |
| case DialogInterface.BUTTON_POSITIVE: { |
| final PhotoViewFragment fragment = |
| (PhotoViewFragment) getSupportFragmentManager().findFragmentByTag(mTag); |
| if (fragment != null) { |
| // commented out because we removed that function |
| // we may need to add it back eventually in a new manner |
| // for now keeping it in |
| // fragment.downloadPhoto(PhotoViewActivity.this, false); |
| } |
| break; |
| } |
| |
| case DialogInterface.BUTTON_NEGATIVE: { |
| break; |
| } |
| } |
| dialog.dismiss(); |
| } |
| } |
| } |