/* * 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.ex.photo; import android.app.ActionBar; import android.app.ActionBar.OnMenuVisibilityListener; import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Build; 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; import android.support.v4.content.Loader; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.text.TextUtils; import android.view.MenuItem; import android.view.View; import com.android.ex.photo.PhotoViewPager.InterceptType; import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener; import com.android.ex.photo.adapters.PhotoPagerAdapter; import com.android.ex.photo.fragments.PhotoViewFragment; import com.android.ex.photo.loaders.PhotoPagerLoader; import com.android.ex.photo.provider.PhotoContract; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Activity to view the contents of an album. */ public class PhotoViewActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks, OnPageChangeListener, OnInterceptTouchListener, OnMenuVisibilityListener, PhotoViewCallbacks { 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 final static String STATE_ACTIONBARTITLE_KEY = "com.google.android.apps.plus.PhotoViewFragment.ACTIONBARTITLE"; private final static String STATE_ACTIONBARSUBTITLE_KEY = "com.google.android.apps.plus.PhotoViewFragment.ACTIONBARSUBTITLE"; private static final int LOADER_PHOTO_LIST = 1; /** Count used when the real photo count is unknown [but, may be determined] */ public static final int ALBUM_COUNT_UNKNOWN = -1; /** 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 URI of the initial photo to display */ private String mInitialPhotoUri; /** The index of the currently viewed photo */ private int mPhotoIndex; /** The query projection to use; may be {@code null} */ private String[] mProjection; /** 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 main root view */ protected View mRootView; /** The main pager; provides left/right swipe between photos */ protected PhotoViewPager mViewPager; /** Adapter to create pager views */ protected PhotoPagerAdapter mAdapter; /** Whether or not we're in "full screen" mode */ private boolean mFullScreen; /** The listeners wanting full screen state for each screen position */ private Map mScreenListeners = new HashMap(); /** The set of listeners wanting full screen state */ private Set mCursorListeners = new HashSet(); /** 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; /** The maximum scale factor applied to images when they are initially displayed */ private float mMaxInitialScale; /** The title in the actionbar */ private String mActionBarTitle; /** The subtitle in the actionbar */ private String mActionBarSubtitle; private final Handler mHandler = new Handler(); // 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. private long mEnterFullScreenDelayTime; protected PhotoPagerAdapter createPhotoPagerAdapter(Context context, android.support.v4.app.FragmentManager fm, Cursor c, float maxScale) { return new PhotoPagerAdapter(context, fm, c, maxScale); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final ActivityManager mgr = (ActivityManager) getApplicationContext(). getSystemService(Activity.ACTIVITY_SERVICE); sMemoryClass = mgr.getMemoryClass(); Intent mIntent = getIntent(); int currentItem = -1; if (savedInstanceState != null) { currentItem = savedInstanceState.getInt(STATE_ITEM_KEY, -1); mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false); mActionBarTitle = savedInstanceState.getString(STATE_ACTIONBARTITLE_KEY); mActionBarSubtitle = savedInstanceState.getString(STATE_ACTIONBARSUBTITLE_KEY); } // 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 // If 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; } // Set the current item from the intent if wasn't in the saved instance if (currentItem < 0) { if (mIntent.hasExtra(Intents.EXTRA_PHOTO_INDEX)) { currentItem = mIntent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1); } if (mIntent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI)) { mInitialPhotoUri = mIntent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI); } } // Set the max initial scale, defaulting to 1x mMaxInitialScale = mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f); // If we still have a negative current item, set it to zero mPhotoIndex = Math.max(currentItem, 0); mIsEmpty = true; setContentView(R.layout.photo_activity_view); // Create the adapter and add the view pager mAdapter = createPhotoPagerAdapter(this, getSupportFragmentManager(), null, mMaxInitialScale); final Resources resources = getResources(); mRootView = findViewById(R.id.photo_activity_root_view); mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager); mViewPager.setAdapter(mAdapter); mViewPager.setOnPageChangeListener(this); mViewPager.setOnInterceptTouchListener(this); mViewPager.setPageMargin(resources.getDimensionPixelSize(R.dimen.photo_page_margin)); // Kick off the loader getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); mEnterFullScreenDelayTime = resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis); final ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.addOnMenuVisibilityListener(this); final int showTitle = ActionBar.DISPLAY_SHOW_TITLE; actionBar.setDisplayOptions(showTitle, showTitle); setActionBarTitles(actionBar); } } @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) { toggleFullScreen(); } else { super.onBackPressed(); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(STATE_ITEM_KEY, mViewPager.getCurrentItem()); outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen); outState.putString(STATE_ACTIONBARTITLE_KEY, mActionBarTitle); outState.putString(STATE_ACTIONBARSUBTITLE_KEY, mActionBarSubtitle); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); default: return super.onOptionsItemSelected(item); } } @Override public void addScreenListener(int position, OnScreenListener listener) { mScreenListeners.put(position, listener); } @Override public void removeScreenListener(int position) { mScreenListeners.remove(position); } @Override public synchronized void addCursorListener(CursorChangedListener listener) { mCursorListeners.add(listener); } @Override public synchronized void removeCursorListener(CursorChangedListener listener) { mCursorListeners.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 void toggleFullScreen() { setFullScreen(!mFullScreen, true); } 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) { finish(); return; } getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this); } @Override public Loader onCreateLoader(int id, Bundle args) { if (id == LOADER_PHOTO_LIST) { return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mProjection); } return null; } @Override public void onLoadFinished(Loader loader, Cursor data) { final int id = loader.getId(); if (id == LOADER_PHOTO_LIST) { if (data == null || data.getCount() == 0) { mIsEmpty = true; } else { mAlbumCount = data.getCount(); if (mInitialPhotoUri != null) { int index = 0; int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI); while (data.moveToNext()) { String uri = data.getString(uriIndex); if (TextUtils.equals(uri, mInitialPhotoUri)) { mInitialPhotoUri = null; mPhotoIndex = index; break; } index++; } } // We're paused; don't do anything now, we'll get re-invoked // when the activity becomes active again // TODO(pwestbro): This shouldn't be necessary, as the loader manager should // restart the loader if (mIsPaused) { mRestartLoader = true; return; } boolean wasEmpty = mIsEmpty; mIsEmpty = false; mAdapter.swapCursor(data); if (mViewPager.getAdapter() == null) { mViewPager.setAdapter(mAdapter); } notifyCursorListeners(data); // 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; } mViewPager.setCurrentItem(itemIndex, false); if (wasEmpty) { setViewActivated(itemIndex); } } // Update the any action items updateActionItems(); } } @Override public void onLoaderReset(android.support.v4.content.Loader loader) { // If the loader is reset, remove the reference in the adapter to this cursor // TODO(pwestbro): reenable this when b/7075236 is fixed // mAdapter.swapCursor(null); } protected void updateActionItems() { // Do nothing, but allow extending classes to do work } private synchronized void notifyCursorListeners(Cursor data) { // tell all of the objects listening for cursor changes // that the cursor has changed for (CursorChangedListener listener : mCursorListeners) { listener.onCursorChanged(data); } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { mPhotoIndex = position; setViewActivated(position); } @Override public void onPageScrollStateChanged(int state) { } @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) { updateActionBar(); } @Override public InterceptType onTouchIntercept(float origX, float origY) { boolean interceptLeft = false; boolean interceptRight = false; for (OnScreenListener listener : mScreenListeners.values()) { if (!interceptLeft) { interceptLeft = listener.onInterceptMoveLeft(origX, origY); } if (!interceptRight) { interceptRight = listener.onInterceptMoveRight(origX, origY); } } 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}. */ protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) { final boolean fullScreenChanged = (fullScreen != mFullScreen); mFullScreen = fullScreen; if (mFullScreen) { setLightsOutMode(true); cancelEnterFullScreenRunnable(); } else { setLightsOutMode(false); if (setDelayedRunnable) { postEnterFullScreenRunnableWithDelay(); } } if (fullScreenChanged) { for (OnScreenListener listener : mScreenListeners.values()) { listener.onFullScreenChanged(mFullScreen); } } } private void postEnterFullScreenRunnableWithDelay() { mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime); } private void cancelEnterFullScreenRunnable() { mHandler.removeCallbacks(mEnterFullScreenRunnable); } protected void setLightsOutMode(boolean enabled) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { int flags = enabled ? View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE : View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; // using mViewPager since we have it and we need a view mViewPager.setSystemUiVisibility(flags); } else { final ActionBar actionBar = getActionBar(); if (enabled) { actionBar.hide(); } else { actionBar.show(); } int flags = enabled ? View.SYSTEM_UI_FLAG_LOW_PROFILE : View.SYSTEM_UI_FLAG_VISIBLE; mViewPager.setSystemUiVisibility(flags); } } private Runnable mEnterFullScreenRunnable = new Runnable() { @Override public void run() { setFullScreen(true, true); } }; @Override public void setViewActivated(int position) { OnScreenListener listener = mScreenListeners.get(position); if (listener != null) { listener.onViewActivated(); } } /** * Adjusts the activity title and subtitle to reflect the photo name and count. */ protected void updateActionBar() { final int position = mViewPager.getCurrentItem() + 1; final boolean hasAlbumCount = mAlbumCount >= 0; final Cursor cursor = getCursorAtProperPosition(); if (cursor != null) { final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME); mActionBarTitle = cursor.getString(photoNameIndex); } else { mActionBarTitle = null; } if (mIsEmpty || !hasAlbumCount || position <= 0) { mActionBarSubtitle = null; } else { mActionBarSubtitle = getResources().getString(R.string.photo_view_count, position, mAlbumCount); } setActionBarTitles(getActionBar()); } /** * Sets the Action Bar title to {@link #mActionBarTitle} and the subtitle to * {@link #mActionBarSubtitle} */ private final void setActionBarTitles(ActionBar actionBar) { if (actionBar == null) { return; } actionBar.setTitle(getInputOrEmpty(mActionBarTitle)); actionBar.setSubtitle(getInputOrEmpty(mActionBarSubtitle)); } /** * If the input string is non-null, it is returned, otherwise an empty string is returned; * @param in * @return */ private static final String getInputOrEmpty(String in) { if (in == null) { return ""; } return in; } /** * 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; } public Cursor getCursor() { return (mAdapter == null) ? null : mAdapter.getCursor(); } @Override public void onMenuVisibilityChanged(boolean isVisible) { if (isVisible) { cancelEnterFullScreenRunnable(); } else { postEnterFullScreenRunnableWithDelay(); } } @Override public void onNewPhotoLoaded(int position) { // do nothing } protected boolean isFullScreen() { return mFullScreen; } protected void setPhotoIndex(int index) { mPhotoIndex = index; } @Override public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) { // do nothing } @Override public PhotoPagerAdapter getAdapter() { return mAdapter; } }