diff options
| author | Ricardo Cerqueira <cyanogenmod@cerqueira.org> | 2013-11-01 16:05:26 +0000 |
|---|---|---|
| committer | Ricardo Cerqueira <cyanogenmod@cerqueira.org> | 2013-11-01 16:05:26 +0000 |
| commit | eb20264840b2045b25e6bd0bb801ac8905ed0498 (patch) | |
| tree | 62e3d6a609af720cd46fdb520afbf01905ccad76 | |
| parent | d98118500eb48b057f5f296cea280a84ac4ede94 (diff) | |
| parent | d54854a4364b03dd1eb5d4787a72da2b430a16a7 (diff) | |
| download | android_frameworks_opt_photoviewer-stable/cm-11.0-XNG3C.tar.gz android_frameworks_opt_photoviewer-stable/cm-11.0-XNG3C.tar.bz2 android_frameworks_opt_photoviewer-stable/cm-11.0-XNG3C.zip | |
Merge tag 'android-4.4_r1' into cm-11.0cm-11.0-XNPH44S-bacon-5fa8c79c0bcm-11.0-XNPH33R-bacon-3628510d76cm-11.0-XNPH30O-bacon-4f280f505acm-11.0-XNPH25R-bacon-d22b777afacm-11.0-XNPH22R-bacon-03d77315eacm-11.0-XNPH05Q-tomato-9828f8e9cccm-11.0-XNPH05Q-bacon-5229c4ef56stable/cm-11.0-XNG3Cstable/cm-11.0-XNG2Sstable/cm-11.0-XNF9Xstable/cm-11.0-XNF8Ystable/cm-11.0shipping/cm-11.0cm-11.0
Android 4.4 Release 1.0
52 files changed, 3575 insertions, 861 deletions
@@ -13,19 +13,47 @@ # limitations under the License. LOCAL_PATH := $(call my-dir) +appcompat_res_dirs := appcompat/res res ../../support/v7/appcompat/res include $(CLEAR_VARS) +LOCAL_MODULE := libphotoviewer_appcompat + +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 \ + android-support-v7-appcompat + +LOCAL_SDK_VERSION := 18 +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-java-files-under, appcompat/src) \ + $(call all-logtags-files-under, src) + +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(appcompat_res_dirs)) +LOCAL_AAPT_FLAGS := --auto-add-overlay + +include $(BUILD_STATIC_JAVA_LIBRARY) + + +include $(CLEAR_VARS) + +activity_res_dirs := activity/res res LOCAL_MODULE := libphotoviewer LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 -LOCAL_SDK_VERSION := 16 +LOCAL_SDK_VERSION := 18 LOCAL_SRC_FILES := \ $(call all-java-files-under, src) \ + $(call all-java-files-under, activity/src) \ $(call all-logtags-files-under, src) -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res + +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(activity_res_dirs)) +LOCAL_AAPT_FLAGS := --auto-add-overlay + + + include $(BUILD_STATIC_JAVA_LIBRARY) + ################################################## # Build all sub-directories diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e4e9101..a1c3f98 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -20,5 +20,5 @@ android:versionCode="1"> <uses-sdk - android:minSdkVersion="11"/> -</manifest>
\ No newline at end of file + android:targetSdkVersion="18" android:minSdkVersion="7"/> +</manifest> diff --git a/res/values/themes.xml b/activity/res/values/themes.xml index 04bec53..04bec53 100644 --- a/res/values/themes.xml +++ b/activity/res/values/themes.xml diff --git a/activity/src/com/android/ex/photo/PhotoViewActivity.java b/activity/src/com/android/ex/photo/PhotoViewActivity.java new file mode 100644 index 0000000..176f722 --- /dev/null +++ b/activity/src/com/android/ex/photo/PhotoViewActivity.java @@ -0,0 +1,1072 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +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.view.MenuItem; + +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.ScaleAnimation; +import android.view.animation.TranslateAnimation; +import android.view.animation.Animation.AnimationListener; +import android.widget.ImageView; + +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.PhotoBitmapLoader; +import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; +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<Cursor>, OnPageChangeListener, OnInterceptTouchListener, + OnMenuVisibilityListener, PhotoViewCallbacks { + + private final static String TAG = "PhotoViewActivity"; + + private final static String STATE_CURRENT_URI_KEY = + "com.google.android.apps.plus.PhotoViewFragment.CURRENT_URI"; + private final static String STATE_CURRENT_INDEX_KEY = + "com.google.android.apps.plus.PhotoViewFragment.CURRENT_INDEX"; + 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.ACTIONBARTITLE"; + private final static String STATE_ENTERANIMATIONFINISHED_KEY = + "com.google.android.apps.plus.PhotoViewFragment.SCALEANIMATIONFINISHED"; + + protected final static String ARG_IMAGE_URI = "image_uri"; + + private static final int LOADER_PHOTO_LIST = 100; + + /** Count used when the real photo count is unknown [but, may be determined] */ + public static final int ALBUM_COUNT_UNKNOWN = -1; + + public static final int ENTER_ANIMATION_DURATION_MS = 250; + public static final int EXIT_ANIMATION_DURATION_MS = 250; + + /** 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 mCurrentPhotoIndex; + /** The uri of the currently viewed photo */ + private String mCurrentPhotoUri; + /** 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}. */ + protected int mAlbumCount = ALBUM_COUNT_UNKNOWN; + /** {@code true} if the view is empty. Otherwise, {@code false}. */ + protected boolean mIsEmpty; + /** the main root view */ + protected View mRootView; + /** Background image that contains nothing, so it can be alpha faded from + * transparent to black without affecting any other views. */ + protected View mBackground; + /** The main pager; provides left/right swipe between photos */ + protected PhotoViewPager mViewPager; + /** The temporary image so that we can quickly scale up the fullscreen thumbnail */ + protected ImageView mTemporaryImage; + /** Adapter to create pager views */ + protected PhotoPagerAdapter mAdapter; + /** Whether or not we're in "full screen" mode */ + protected boolean mFullScreen; + /** The listeners wanting full screen state for each screen position */ + private final Map<Integer, OnScreenListener> + mScreenListeners = new HashMap<Integer, OnScreenListener>(); + /** The set of listeners wanting full screen state */ + private final Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>(); + /** When {@code true}, restart the loader when the activity becomes active */ + private boolean mRestartLoader; + /** Whether or not this activity is paused */ + protected boolean mIsPaused = true; + /** The maximum scale factor applied to images when they are initially displayed */ + protected float mMaxInitialScale; + /** The title in the actionbar */ + protected String mActionBarTitle; + /** The subtitle in the actionbar */ + protected String mActionBarSubtitle; + + private boolean mEnterAnimationFinished; + protected boolean mScaleAnimationEnabled; + protected int mAnimationStartX; + protected int mAnimationStartY; + protected int mAnimationStartWidth; + protected int mAnimationStartHeight; + + protected boolean mActionBarHiddenInitially; + protected boolean mDisplayThumbsFullScreen; + + protected BitmapCallback mBitmapCallback; + protected 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) { + PhotoPagerAdapter adapter = new PhotoPagerAdapter(context, fm, c, maxScale, + mDisplayThumbsFullScreen); + return adapter; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final ActivityManager mgr = (ActivityManager) getApplicationContext(). + getSystemService(Activity.ACTIVITY_SERVICE); + sMemoryClass = mgr.getMemoryClass(); + + final Intent intent = getIntent(); + // uri of the photos to view; optional + if (intent.hasExtra(Intents.EXTRA_PHOTOS_URI)) { + mPhotosUri = intent.getStringExtra(Intents.EXTRA_PHOTOS_URI); + } + if (intent.getBooleanExtra(Intents.EXTRA_SCALE_UP_ANIMATION, false)) { + mScaleAnimationEnabled = true; + mAnimationStartX = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_X, 0); + mAnimationStartY = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_Y, 0); + mAnimationStartWidth = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_WIDTH, 0); + mAnimationStartHeight = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_HEIGHT, 0); + } + mActionBarHiddenInitially = intent.getBooleanExtra( + Intents.EXTRA_ACTION_BAR_HIDDEN_INITIALLY, false); + mDisplayThumbsFullScreen = intent.getBooleanExtra( + Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false); + + // projection for the query; optional + // If not set, the default projection is used. + // This projection must include the columns from the default projection. + if (intent.hasExtra(Intents.EXTRA_PROJECTION)) { + mProjection = intent.getStringArrayExtra(Intents.EXTRA_PROJECTION); + } else { + mProjection = null; + } + + // Set the max initial scale, defaulting to 1x + mMaxInitialScale = intent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f); + mCurrentPhotoUri = null; + mCurrentPhotoIndex = -1; + + // We allow specifying the current photo by either index or uri. + // This is because some users may have live datasets that can change, + // adding new items to either the beginning or end of the set. For clients + // that do not need that capability, ability to specify the current photo + // by index is offered as a convenience. + if (intent.hasExtra(Intents.EXTRA_PHOTO_INDEX)) { + mCurrentPhotoIndex = intent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1); + } + if (intent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI)) { + mCurrentPhotoUri = intent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI); + } + mIsEmpty = true; + + if (savedInstanceState != null) { + mCurrentPhotoUri = savedInstanceState.getString(STATE_CURRENT_URI_KEY); + mCurrentPhotoIndex = savedInstanceState.getInt(STATE_CURRENT_INDEX_KEY); + mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false); + mActionBarTitle = savedInstanceState.getString(STATE_ACTIONBARTITLE_KEY); + mActionBarSubtitle = savedInstanceState.getString(STATE_ACTIONBARSUBTITLE_KEY); + mEnterAnimationFinished = savedInstanceState.getBoolean( + STATE_ENTERANIMATIONFINISHED_KEY, false); + } else { + mFullScreen = mActionBarHiddenInitially; + } + + 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); + mBackground = findViewById(R.id.photo_activity_background); + mTemporaryImage = (ImageView) findViewById(R.id.photo_activity_temporary_image); + 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)); + + mBitmapCallback = new BitmapCallback(); + if (!mScaleAnimationEnabled || mEnterAnimationFinished) { + // We are not running the scale up animation. Just let the fragments + // display and handle the animation. + getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); + // Make the background opaque immediately so that we don't see the activity + // behind this one. + mBackground.setVisibility(View.VISIBLE); + } else { + // Attempt to load the initial image thumbnail. Once we have the + // image, animate it up. Once the animation is complete, we can kick off + // loading the ViewPager. After the primary fullres image is loaded, we will + // make our temporary image invisible and display the ViewPager. + mViewPager.setVisibility(View.GONE); + Bundle args = new Bundle(); + args.putString(ARG_IMAGE_URI, mCurrentPhotoUri); + getSupportLoaderManager().initLoader(BITMAP_LOADER_THUMBNAIL, args, mBitmapCallback); + } + + 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); + // Set the title and subtitle immediately here, rather than waiting + // for the fragment to be initialized. + setActionBarTitles(actionBar); + } + + setLightsOutMode(mFullScreen); + } + + @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 we are in fullscreen mode, and the default is not full screen, then + // switch back to actionBar display mode. + if (mFullScreen && !mActionBarHiddenInitially) { + toggleFullScreen(); + } else { + if (mScaleAnimationEnabled) { + runExitAnimation(); + } else { + super.onBackPressed(); + } + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(STATE_CURRENT_URI_KEY, mCurrentPhotoUri); + outState.putInt(STATE_CURRENT_INDEX_KEY, mCurrentPhotoIndex); + outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen); + outState.putString(STATE_ACTIONBARTITLE_KEY, mActionBarTitle); + outState.putString(STATE_ACTIONBARSUBTITLE_KEY, mActionBarSubtitle); + outState.putBoolean(STATE_ENTERANIMATIONFINISHED_KEY, mEnterAnimationFinished); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + 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<Cursor> onCreateLoader(int id, Bundle args) { + if (id == LOADER_PHOTO_LIST) { + return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mProjection); + } + return null; + } + + @Override + public Loader<BitmapResult> onCreateBitmapLoader(int id, Bundle args, String uri) { + switch (id) { + case BITMAP_LOADER_AVATAR: + case BITMAP_LOADER_THUMBNAIL: + case BITMAP_LOADER_PHOTO: + return new PhotoBitmapLoader(this, uri); + default: + return null; + } + } + + @Override + public void onLoadFinished(Loader<Cursor> 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 (mCurrentPhotoUri != null) { + int index = 0; + // Clear query params. Compare only the path. + final int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI); + final Uri currentPhotoUri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() + .clearQuery().build(); + } else { + currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() + .query(null).build(); + } + while (data.moveToNext()) { + final String uriString = data.getString(uriIndex); + final Uri uri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + uri = Uri.parse(uriString).buildUpon().clearQuery().build(); + } else { + uri = Uri.parse(uriString).buildUpon().query(null).build(); + } + if (currentPhotoUri != null && currentPhotoUri.equals(uri)) { + mCurrentPhotoIndex = 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); + + // Use an index of 0 if the index wasn't specified or couldn't be found + if (mCurrentPhotoIndex < 0) { + mCurrentPhotoIndex = 0; + } + + mViewPager.setCurrentItem(mCurrentPhotoIndex, false); + if (wasEmpty) { + setViewActivated(mCurrentPhotoIndex); + } + } + // Update the any action items + updateActionItems(); + } + } + + @Override + public void onLoaderReset(android.support.v4.content.Loader<Cursor> 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) { + mCurrentPhotoIndex = 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(PhotoViewFragment fragment) { + // Do nothing, we handle this in setViewActivated + } + + @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) { + int flags = 0; + final int version = Build.VERSION.SDK_INT; + final ActionBar actionBar = getActionBar(); + if (enabled) { + if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + flags = View.SYSTEM_UI_FLAG_LOW_PROFILE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + if (!mScaleAnimationEnabled) { + // If we are using the scale animation for intro and exit, + // we can't go into fullscreen mode. The issue is that the + // activity that invoked this will not be in fullscreen, so + // as we transition out, the background activity will be + // temporarily rendered without an actionbar, and the shrinking + // photo will not line up properly. After that it redraws + // in the correct location, but it still looks janks. + // FLAG: there may be a better way to fix this, but I don't + // yet know what it is. + flags |= View.SYSTEM_UI_FLAG_FULLSCREEN; + } + } else if (version >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + flags = View.SYSTEM_UI_FLAG_LOW_PROFILE; + } else if (version >= android.os.Build.VERSION_CODES.HONEYCOMB) { + flags = View.STATUS_BAR_HIDDEN; + } + actionBar.hide(); + } else { + if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + } else if (version >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + flags = View.SYSTEM_UI_FLAG_VISIBLE; + } else if (version >= android.os.Build.VERSION_CODES.HONEYCOMB) { + flags = View.STATUS_BAR_VISIBLE; + } + actionBar.show(); + } + + if (version >= Build.VERSION_CODES.HONEYCOMB) { + mRootView.setSystemUiVisibility(flags); + } + } + + private final 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(); + } + final Cursor cursor = getCursorAtProperPosition(); + mCurrentPhotoIndex = position; + // FLAG: get the column indexes once in onLoadFinished(). + // That would make this more efficient, instead of looking these up + // repeatedly whenever we want them. + int uriIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI); + mCurrentPhotoUri = cursor.getString(uriIndex); + updateActionBar(); + + // Restart the timer to return to fullscreen. + cancelEnterFullScreenRunnable(); + postEnterFullScreenRunnableWithDelay(); + } + + /** + * 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) { + // FLAG: We should grab the indexes when we first get the cursor + // and store them so we don't need to do it each time. + 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} + */ + protected 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 void setPhotoIndex(int index) { + mCurrentPhotoIndex = index; + } + + @Override + public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success) { + if (mTemporaryImage.getVisibility() != View.GONE && + TextUtils.equals(fragment.getPhotoUri(), mCurrentPhotoUri)) { + if (success) { + // The fragment for the current image is now ready for display. + mTemporaryImage.setVisibility(View.GONE); + mViewPager.setVisibility(View.VISIBLE); + } else { + // This means that we are unable to load the fragment's photo. + // I'm not sure what the best thing to do here is, but at least if + // we display the viewPager, the fragment itself can decide how to + // display the failure of its own image. + Log.w(TAG, "Failed to load fragment image"); + mTemporaryImage.setVisibility(View.GONE); + mViewPager.setVisibility(View.VISIBLE); + } + } + } + + protected boolean isFullScreen() { + return mFullScreen; + } + + @Override + public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) { + // do nothing + } + + @Override + public PhotoPagerAdapter getAdapter() { + return mAdapter; + } + + public void onEnterAnimationComplete() { + mEnterAnimationFinished = true; + mViewPager.setVisibility(View.VISIBLE); + } + + private void onExitAnimationComplete() { + finish(); + overridePendingTransition(0, 0); + } + + private void runEnterAnimation() { + final int totalWidth = mRootView.getMeasuredWidth(); + final int totalHeight = mRootView.getMeasuredHeight(); + + // FLAG: Need to handle the aspect ratio of the bitmap. If it's a portrait + // bitmap, then we need to position the view higher so that the middle + // pixels line up. + mTemporaryImage.setVisibility(View.VISIBLE); + // We need to take a full screen image, and scale/translate it so that + // it appears at exactly the same location onscreen as it is in the + // prior activity. + // The final image will take either the full screen width or height (or both). + + final float scaleW = (float) mAnimationStartWidth / totalWidth; + final float scaleY = (float) mAnimationStartHeight / totalHeight; + final float scale = Math.max(scaleW, scaleY); + + final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, + totalWidth, scale); + final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, + totalHeight, scale); + + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + mBackground.setAlpha(0f); + mBackground.animate().alpha(1f).setDuration(ENTER_ANIMATION_DURATION_MS).start(); + mBackground.setVisibility(View.VISIBLE); + + mTemporaryImage.setScaleX(scale); + mTemporaryImage.setScaleY(scale); + mTemporaryImage.setTranslationX(translateX); + mTemporaryImage.setTranslationY(translateY); + + Runnable endRunnable = new Runnable() { + @Override + public void run() { + PhotoViewActivity.this.onEnterAnimationComplete(); + } + }; + ViewPropertyAnimator animator = mTemporaryImage.animate().scaleX(1f).scaleY(1f) + .translationX(0).translationY(0).setDuration(ENTER_ANIMATION_DURATION_MS); + if (version >= Build.VERSION_CODES.JELLY_BEAN) { + animator.withEndAction(endRunnable); + } else { + mHandler.postDelayed(endRunnable, ENTER_ANIMATION_DURATION_MS); + } + animator.start(); + } else { + final Animation alphaAnimation = new AlphaAnimation(0f, 1f); + alphaAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); + mBackground.startAnimation(alphaAnimation); + mBackground.setVisibility(View.VISIBLE); + + final Animation translateAnimation = new TranslateAnimation(translateX, + translateY, 0, 0); + translateAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); + Animation scaleAnimation = new ScaleAnimation(scale, scale, 0, 0); + scaleAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); + + AnimationSet animationSet = new AnimationSet(true); + animationSet.addAnimation(translateAnimation); + animationSet.addAnimation(scaleAnimation); + AnimationListener listener = new AnimationListener() { + @Override + public void onAnimationEnd(Animation arg0) { + PhotoViewActivity.this.onEnterAnimationComplete(); + } + + @Override + public void onAnimationRepeat(Animation arg0) { + } + + @Override + public void onAnimationStart(Animation arg0) { + } + }; + animationSet.setAnimationListener(listener); + mTemporaryImage.startAnimation(animationSet); + } + } + + private void runExitAnimation() { + Intent intent = getIntent(); + // FLAG: should just fall back to a standard animation if either: + // 1. images have been added or removed since we've been here, or + // 2. we are currently looking at some image other than the one we + // started on. + + final int totalWidth = mRootView.getMeasuredWidth(); + final int totalHeight = mRootView.getMeasuredHeight(); + + // We need to take a full screen image, and scale/translate it so that + // it appears at exactly the same location onscreen as it is in the + // prior activity. + // The final image will take either the full screen width or height (or both). + final float scaleW = (float) mAnimationStartWidth / totalWidth; + final float scaleY = (float) mAnimationStartHeight / totalHeight; + final float scale = Math.max(scaleW, scaleY); + + final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, + totalWidth, scale); + final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, + totalHeight, scale); + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + mBackground.animate().alpha(0f).setDuration(EXIT_ANIMATION_DURATION_MS).start(); + mBackground.setVisibility(View.VISIBLE); + + Runnable endRunnable = new Runnable() { + @Override + public void run() { + PhotoViewActivity.this.onExitAnimationComplete(); + } + }; + // If the temporary image is still visible it means that we have + // not yet loaded the fullres image, so we need to animate + // the temporary image out. + ViewPropertyAnimator animator = null; + if (mTemporaryImage.getVisibility() == View.VISIBLE) { + animator = mTemporaryImage.animate().scaleX(scale).scaleY(scale) + .translationX(translateX).translationY(translateY) + .setDuration(EXIT_ANIMATION_DURATION_MS); + } else { + animator = mViewPager.animate().scaleX(scale).scaleY(scale) + .translationX(translateX).translationY(translateY) + .setDuration(EXIT_ANIMATION_DURATION_MS); + } + if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + animator.withEndAction(endRunnable); + } else { + mHandler.postDelayed(endRunnable, EXIT_ANIMATION_DURATION_MS); + } + animator.start(); + } else { + final Animation alphaAnimation = new AlphaAnimation(1f, 0f); + alphaAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); + mBackground.startAnimation(alphaAnimation); + mBackground.setVisibility(View.VISIBLE); + + final Animation scaleAnimation = new ScaleAnimation(1f, 1f, scale, scale); + scaleAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); + AnimationListener listener = new AnimationListener() { + @Override + public void onAnimationEnd(Animation arg0) { + PhotoViewActivity.this.onExitAnimationComplete(); + } + + @Override + public void onAnimationRepeat(Animation arg0) { + } + + @Override + public void onAnimationStart(Animation arg0) { + } + }; + scaleAnimation.setAnimationListener(listener); + // If the temporary image is still visible it means that we have + // not yet loaded the fullres image, so we need to animate + // the temporary image out. + if (mTemporaryImage.getVisibility() == View.VISIBLE) { + mTemporaryImage.startAnimation(scaleAnimation); + } else { + mViewPager.startAnimation(scaleAnimation); + } + } + } + + private int calculateTranslate(int start, int startSize, int totalSize, float scale) { + // Translation takes precedence over scale. What this means is that if + // we want an view's upper left corner to be a particular spot on screen, + // but that view is scaled to something other than 1, we need to take into + // account the pixels lost to scaling. + // So if we have a view that is 200x300, and we want it's upper left corner + // to be at 50x50, but it's scaled by 50%, we can't just translate it to 50x50. + // If we were to do that, the view's *visible* upper left corner would be at + // 100x200. We need to take into account the difference between the outside + // size of the view (i.e. the size prior to scaling) and the scaled size. + // scaleFromEdge is the difference between the visible left edge and the + // actual left edge, due to scaling. + // scaleFromTop is the difference between the visible top edge, and the + // actual top edge, due to scaling. + int scaleFromEdge = Math.round((totalSize - totalSize * scale) / 2); + + // The imageView is fullscreen, regardless of the aspect ratio of the actual image. + // This means that some portion of the imageView will be blank. We need to + // take into account the size of the blank area so that the actual image + // lines up with the starting image. + int blankSize = Math.round((totalSize * scale - startSize) / 2); + + return start - scaleFromEdge - blankSize; + } + + private void initTemporaryImage(Bitmap bitmap) { + if (mEnterAnimationFinished) { + // Forget this, we've already run the animation. + return; + } + mTemporaryImage.setImageBitmap(bitmap); + if (bitmap != null) { + // We have not yet run the enter animation. Start it now. + int totalWidth = mRootView.getMeasuredWidth(); + if (totalWidth == 0) { + // the measure pass has not yet finished. We can't properly + // run out animation until that is done. Listen for the layout + // to occur, then fire the animation. + final View base = mRootView; + base.getViewTreeObserver().addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + int version = android.os.Build.VERSION.SDK_INT; + if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + base.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } else { + base.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + runEnterAnimation(); + } + }); + } else { + // initiate the animation + runEnterAnimation(); + } + } + // Kick off the photo list loader + getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); + } + + private class BitmapCallback implements LoaderManager.LoaderCallbacks<BitmapResult> { + + @Override + public Loader<BitmapResult> onCreateLoader(int id, Bundle args) { + String uri = args.getString(ARG_IMAGE_URI); + switch (id) { + case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: + return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, + args, uri); + case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: + return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_AVATAR, + args, uri); + } + return null; + } + + @Override + public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) { + Bitmap bitmap = result.bitmap; + final ActionBar actionBar = getActionBar(); + switch (loader.getId()) { + case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: + // We just loaded the initial thumbnail that we can display + // while waiting for the full viewPager to get initialized. + initTemporaryImage(bitmap); + // Destroy the loader so we don't attempt to load the thumbnail + // again on screen rotations. + getSupportLoaderManager().destroyLoader( + PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL); + break; + case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: + if (bitmap == null) { + actionBar.setLogo(null); + } else { + BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); + actionBar.setLogo(drawable); + } + break; + } + } + + @Override + public void onLoaderReset(Loader<BitmapResult> loader) { + // Do nothing + } + } +} diff --git a/appcompat/res/values/themes.xml b/appcompat/res/values/themes.xml new file mode 100644 index 0000000..6a5c158 --- /dev/null +++ b/appcompat/res/values/themes.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 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. +--> + +<resources> + <style name="PhotoViewTheme" parent="Theme.AppCompat"> + <item name="android:windowNoTitle">false</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowActionBarOverlay">true</item> + <item name="android:windowBackground">@color/solid_black</item> + <item name="actionBarStyle">@style/AppCompat.ActionBar</item> + <item name="android:actionBarStyle">@style/AppCompat.ActionBar</item> + </style> + <style name="AppCompat.ActionBar" parent="Widget.AppCompat.ActionBar"> + <item name="background">@drawable/actionbar_translucent</item> + <item name="android:background">@drawable/actionbar_translucent</item> + </style> +</resources> diff --git a/appcompat/src/com/android/ex/photo/PhotoViewActivity.java b/appcompat/src/com/android/ex/photo/PhotoViewActivity.java new file mode 100644 index 0000000..58309ad --- /dev/null +++ b/appcompat/src/com/android/ex/photo/PhotoViewActivity.java @@ -0,0 +1,1071 @@ +/* + * 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.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +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.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.view.MenuItem; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBar.OnMenuVisibilityListener; +import android.support.v7.app.ActionBarActivity; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.ScaleAnimation; +import android.view.animation.TranslateAnimation; +import android.view.animation.Animation.AnimationListener; +import android.widget.ImageView; + +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.PhotoBitmapLoader; +import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; +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 ActionBarActivity implements + LoaderManager.LoaderCallbacks<Cursor>, OnPageChangeListener, OnInterceptTouchListener, + OnMenuVisibilityListener, PhotoViewCallbacks { + + private final static String TAG = "PhotoViewActivity"; + + private final static String STATE_CURRENT_URI_KEY = + "com.google.android.apps.plus.PhotoViewFragment.CURRENT_URI"; + private final static String STATE_CURRENT_INDEX_KEY = + "com.google.android.apps.plus.PhotoViewFragment.CURRENT_INDEX"; + 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.ACTIONBARTITLE"; + private final static String STATE_ENTERANIMATIONFINISHED_KEY = + "com.google.android.apps.plus.PhotoViewFragment.SCALEANIMATIONFINISHED"; + + protected final static String ARG_IMAGE_URI = "image_uri"; + + private static final int LOADER_PHOTO_LIST = 100; + + /** Count used when the real photo count is unknown [but, may be determined] */ + public static final int ALBUM_COUNT_UNKNOWN = -1; + + public static final int ENTER_ANIMATION_DURATION_MS = 250; + public static final int EXIT_ANIMATION_DURATION_MS = 250; + + /** 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 mCurrentPhotoIndex; + /** The uri of the currently viewed photo */ + private String mCurrentPhotoUri; + /** 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}. */ + protected int mAlbumCount = ALBUM_COUNT_UNKNOWN; + /** {@code true} if the view is empty. Otherwise, {@code false}. */ + protected boolean mIsEmpty; + /** the main root view */ + protected View mRootView; + /** Background image that contains nothing, so it can be alpha faded from + * transparent to black without affecting any other views. */ + protected View mBackground; + /** The main pager; provides left/right swipe between photos */ + protected PhotoViewPager mViewPager; + /** The temporary image so that we can quickly scale up the fullscreen thumbnail */ + protected ImageView mTemporaryImage; + /** Adapter to create pager views */ + protected PhotoPagerAdapter mAdapter; + /** Whether or not we're in "full screen" mode */ + protected boolean mFullScreen; + /** The listeners wanting full screen state for each screen position */ + private final Map<Integer, OnScreenListener> + mScreenListeners = new HashMap<Integer, OnScreenListener>(); + /** The set of listeners wanting full screen state */ + private final Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>(); + /** When {@code true}, restart the loader when the activity becomes active */ + private boolean mRestartLoader; + /** Whether or not this activity is paused */ + protected boolean mIsPaused = true; + /** The maximum scale factor applied to images when they are initially displayed */ + protected float mMaxInitialScale; + /** The title in the actionbar */ + protected String mActionBarTitle; + /** The subtitle in the actionbar */ + protected String mActionBarSubtitle; + + private boolean mEnterAnimationFinished; + protected boolean mScaleAnimationEnabled; + protected int mAnimationStartX; + protected int mAnimationStartY; + protected int mAnimationStartWidth; + protected int mAnimationStartHeight; + + protected boolean mActionBarHiddenInitially; + protected boolean mDisplayThumbsFullScreen; + + protected BitmapCallback mBitmapCallback; + protected 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) { + PhotoPagerAdapter adapter = new PhotoPagerAdapter(context, fm, c, maxScale, + mDisplayThumbsFullScreen); + return adapter; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final ActivityManager mgr = (ActivityManager) getApplicationContext(). + getSystemService(Activity.ACTIVITY_SERVICE); + sMemoryClass = mgr.getMemoryClass(); + + final Intent intent = getIntent(); + // uri of the photos to view; optional + if (intent.hasExtra(Intents.EXTRA_PHOTOS_URI)) { + mPhotosUri = intent.getStringExtra(Intents.EXTRA_PHOTOS_URI); + } + if (intent.getBooleanExtra(Intents.EXTRA_SCALE_UP_ANIMATION, false)) { + mScaleAnimationEnabled = true; + mAnimationStartX = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_X, 0); + mAnimationStartY = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_Y, 0); + mAnimationStartWidth = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_WIDTH, 0); + mAnimationStartHeight = intent.getIntExtra(Intents.EXTRA_ANIMATION_START_HEIGHT, 0); + } + mActionBarHiddenInitially = intent.getBooleanExtra( + Intents.EXTRA_ACTION_BAR_HIDDEN_INITIALLY, false); + mDisplayThumbsFullScreen = intent.getBooleanExtra( + Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false); + + // projection for the query; optional + // If not set, the default projection is used. + // This projection must include the columns from the default projection. + if (intent.hasExtra(Intents.EXTRA_PROJECTION)) { + mProjection = intent.getStringArrayExtra(Intents.EXTRA_PROJECTION); + } else { + mProjection = null; + } + + // Set the max initial scale, defaulting to 1x + mMaxInitialScale = intent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f); + mCurrentPhotoUri = null; + mCurrentPhotoIndex = -1; + + // We allow specifying the current photo by either index or uri. + // This is because some users may have live datasets that can change, + // adding new items to either the beginning or end of the set. For clients + // that do not need that capability, ability to specify the current photo + // by index is offered as a convenience. + if (intent.hasExtra(Intents.EXTRA_PHOTO_INDEX)) { + mCurrentPhotoIndex = intent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1); + } + if (intent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI)) { + mCurrentPhotoUri = intent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI); + } + mIsEmpty = true; + + if (savedInstanceState != null) { + mCurrentPhotoUri = savedInstanceState.getString(STATE_CURRENT_URI_KEY); + mCurrentPhotoIndex = savedInstanceState.getInt(STATE_CURRENT_INDEX_KEY); + mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false); + mActionBarTitle = savedInstanceState.getString(STATE_ACTIONBARTITLE_KEY); + mActionBarSubtitle = savedInstanceState.getString(STATE_ACTIONBARSUBTITLE_KEY); + mEnterAnimationFinished = savedInstanceState.getBoolean( + STATE_ENTERANIMATIONFINISHED_KEY, false); + } else { + mFullScreen = mActionBarHiddenInitially; + } + + 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); + mBackground = findViewById(R.id.photo_activity_background); + mTemporaryImage = (ImageView) findViewById(R.id.photo_activity_temporary_image); + 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)); + + mBitmapCallback = new BitmapCallback(); + if (!mScaleAnimationEnabled || mEnterAnimationFinished) { + // We are not running the scale up animation. Just let the fragments + // display and handle the animation. + getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); + // Make the background opaque immediately so that we don't see the activity + // behind this one. + mBackground.setVisibility(View.VISIBLE); + } else { + // Attempt to load the initial image thumbnail. Once we have the + // image, animate it up. Once the animation is complete, we can kick off + // loading the ViewPager. After the primary fullres image is loaded, we will + // make our temporary image invisible and display the ViewPager. + mViewPager.setVisibility(View.GONE); + Bundle args = new Bundle(); + args.putString(ARG_IMAGE_URI, mCurrentPhotoUri); + getSupportLoaderManager().initLoader(BITMAP_LOADER_THUMBNAIL, args, mBitmapCallback); + } + + mEnterFullScreenDelayTime = + resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis); + + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.addOnMenuVisibilityListener(this); + final int showTitle = ActionBar.DISPLAY_SHOW_TITLE; + actionBar.setDisplayOptions(showTitle, showTitle); + // Set the title and subtitle immediately here, rather than waiting + // for the fragment to be initialized. + setActionBarTitles(actionBar); + } + + setLightsOutMode(mFullScreen); + } + + @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 we are in fullscreen mode, and the default is not full screen, then + // switch back to actionBar display mode. + if (mFullScreen && !mActionBarHiddenInitially) { + toggleFullScreen(); + } else { + if (mScaleAnimationEnabled) { + runExitAnimation(); + } else { + super.onBackPressed(); + } + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(STATE_CURRENT_URI_KEY, mCurrentPhotoUri); + outState.putInt(STATE_CURRENT_INDEX_KEY, mCurrentPhotoIndex); + outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen); + outState.putString(STATE_ACTIONBARTITLE_KEY, mActionBarTitle); + outState.putString(STATE_ACTIONBARSUBTITLE_KEY, mActionBarSubtitle); + outState.putBoolean(STATE_ENTERANIMATIONFINISHED_KEY, mEnterAnimationFinished); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + 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<Cursor> onCreateLoader(int id, Bundle args) { + if (id == LOADER_PHOTO_LIST) { + return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mProjection); + } + return null; + } + + @Override + public Loader<BitmapResult> onCreateBitmapLoader(int id, Bundle args, String uri) { + switch (id) { + case BITMAP_LOADER_AVATAR: + case BITMAP_LOADER_THUMBNAIL: + case BITMAP_LOADER_PHOTO: + return new PhotoBitmapLoader(this, uri); + default: + return null; + } + } + + @Override + public void onLoadFinished(Loader<Cursor> 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 (mCurrentPhotoUri != null) { + int index = 0; + // Clear query params. Compare only the path. + final int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI); + final Uri currentPhotoUri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() + .clearQuery().build(); + } else { + currentPhotoUri = Uri.parse(mCurrentPhotoUri).buildUpon() + .query(null).build(); + } + while (data.moveToNext()) { + final String uriString = data.getString(uriIndex); + final Uri uri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + uri = Uri.parse(uriString).buildUpon().clearQuery().build(); + } else { + uri = Uri.parse(uriString).buildUpon().query(null).build(); + } + if (currentPhotoUri != null && currentPhotoUri.equals(uri)) { + mCurrentPhotoIndex = 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); + + // Use an index of 0 if the index wasn't specified or couldn't be found + if (mCurrentPhotoIndex < 0) { + mCurrentPhotoIndex = 0; + } + + mViewPager.setCurrentItem(mCurrentPhotoIndex, false); + if (wasEmpty) { + setViewActivated(mCurrentPhotoIndex); + } + } + // Update the any action items + updateActionItems(); + } + } + + @Override + public void onLoaderReset(android.support.v4.content.Loader<Cursor> 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) { + mCurrentPhotoIndex = 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(PhotoViewFragment fragment) { + // Do nothing, we handle this in setViewActivated + } + + @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) { + int flags = 0; + final int version = Build.VERSION.SDK_INT; + final ActionBar actionBar = getSupportActionBar(); + if (enabled) { + if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + flags = View.SYSTEM_UI_FLAG_LOW_PROFILE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + if (!mScaleAnimationEnabled) { + // If we are using the scale animation for intro and exit, + // we can't go into fullscreen mode. The issue is that the + // activity that invoked this will not be in fullscreen, so + // as we transition out, the background activity will be + // temporarily rendered without an actionbar, and the shrinking + // photo will not line up properly. After that it redraws + // in the correct location, but it still looks janks. + // FLAG: there may be a better way to fix this, but I don't + // yet know what it is. + flags |= View.SYSTEM_UI_FLAG_FULLSCREEN; + } + } else if (version >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + flags = View.SYSTEM_UI_FLAG_LOW_PROFILE; + } else if (version >= android.os.Build.VERSION_CODES.HONEYCOMB) { + flags = View.STATUS_BAR_HIDDEN; + } + actionBar.hide(); + } else { + if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + } else if (version >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + flags = View.SYSTEM_UI_FLAG_VISIBLE; + } else if (version >= android.os.Build.VERSION_CODES.HONEYCOMB) { + flags = View.STATUS_BAR_VISIBLE; + } + actionBar.show(); + } + + if (version >= Build.VERSION_CODES.HONEYCOMB) { + mRootView.setSystemUiVisibility(flags); + } + } + + private final 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(); + } + final Cursor cursor = getCursorAtProperPosition(); + mCurrentPhotoIndex = position; + // FLAG: get the column indexes once in onLoadFinished(). + // That would make this more efficient, instead of looking these up + // repeatedly whenever we want them. + int uriIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI); + mCurrentPhotoUri = cursor.getString(uriIndex); + updateActionBar(); + + // Restart the timer to return to fullscreen. + cancelEnterFullScreenRunnable(); + postEnterFullScreenRunnableWithDelay(); + } + + /** + * 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) { + // FLAG: We should grab the indexes when we first get the cursor + // and store them so we don't need to do it each time. + 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(getSupportActionBar()); + } + + /** + * Sets the Action Bar title to {@link #mActionBarTitle} and the subtitle to + * {@link #mActionBarSubtitle} + */ + protected 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 void setPhotoIndex(int index) { + mCurrentPhotoIndex = index; + } + + @Override + public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment, boolean success) { + if (mTemporaryImage.getVisibility() != View.GONE && + TextUtils.equals(fragment.getPhotoUri(), mCurrentPhotoUri)) { + if (success) { + // The fragment for the current image is now ready for display. + mTemporaryImage.setVisibility(View.GONE); + mViewPager.setVisibility(View.VISIBLE); + } else { + // This means that we are unable to load the fragment's photo. + // I'm not sure what the best thing to do here is, but at least if + // we display the viewPager, the fragment itself can decide how to + // display the failure of its own image. + Log.w(TAG, "Failed to load fragment image"); + mTemporaryImage.setVisibility(View.GONE); + mViewPager.setVisibility(View.VISIBLE); + } + } + } + + protected boolean isFullScreen() { + return mFullScreen; + } + + @Override + public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor) { + // do nothing + } + + @Override + public PhotoPagerAdapter getAdapter() { + return mAdapter; + } + + public void onEnterAnimationComplete() { + mEnterAnimationFinished = true; + mViewPager.setVisibility(View.VISIBLE); + } + + private void onExitAnimationComplete() { + finish(); + overridePendingTransition(0, 0); + } + + private void runEnterAnimation() { + final int totalWidth = mRootView.getMeasuredWidth(); + final int totalHeight = mRootView.getMeasuredHeight(); + + // FLAG: Need to handle the aspect ratio of the bitmap. If it's a portrait + // bitmap, then we need to position the view higher so that the middle + // pixels line up. + mTemporaryImage.setVisibility(View.VISIBLE); + // We need to take a full screen image, and scale/translate it so that + // it appears at exactly the same location onscreen as it is in the + // prior activity. + // The final image will take either the full screen width or height (or both). + + final float scaleW = (float) mAnimationStartWidth / totalWidth; + final float scaleY = (float) mAnimationStartHeight / totalHeight; + final float scale = Math.max(scaleW, scaleY); + + final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, + totalWidth, scale); + final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, + totalHeight, scale); + + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + mBackground.setAlpha(0f); + mBackground.animate().alpha(1f).setDuration(ENTER_ANIMATION_DURATION_MS).start(); + mBackground.setVisibility(View.VISIBLE); + + mTemporaryImage.setScaleX(scale); + mTemporaryImage.setScaleY(scale); + mTemporaryImage.setTranslationX(translateX); + mTemporaryImage.setTranslationY(translateY); + + Runnable endRunnable = new Runnable() { + @Override + public void run() { + PhotoViewActivity.this.onEnterAnimationComplete(); + } + }; + ViewPropertyAnimator animator = mTemporaryImage.animate().scaleX(1f).scaleY(1f) + .translationX(0).translationY(0).setDuration(ENTER_ANIMATION_DURATION_MS); + if (version >= Build.VERSION_CODES.JELLY_BEAN) { + animator.withEndAction(endRunnable); + } else { + mHandler.postDelayed(endRunnable, ENTER_ANIMATION_DURATION_MS); + } + animator.start(); + } else { + final Animation alphaAnimation = new AlphaAnimation(0f, 1f); + alphaAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); + mBackground.startAnimation(alphaAnimation); + mBackground.setVisibility(View.VISIBLE); + + final Animation translateAnimation = new TranslateAnimation(translateX, + translateY, 0, 0); + translateAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); + Animation scaleAnimation = new ScaleAnimation(scale, scale, 0, 0); + scaleAnimation.setDuration(ENTER_ANIMATION_DURATION_MS); + + AnimationSet animationSet = new AnimationSet(true); + animationSet.addAnimation(translateAnimation); + animationSet.addAnimation(scaleAnimation); + AnimationListener listener = new AnimationListener() { + @Override + public void onAnimationEnd(Animation arg0) { + PhotoViewActivity.this.onEnterAnimationComplete(); + } + + @Override + public void onAnimationRepeat(Animation arg0) { + } + + @Override + public void onAnimationStart(Animation arg0) { + } + }; + animationSet.setAnimationListener(listener); + mTemporaryImage.startAnimation(animationSet); + } + } + + private void runExitAnimation() { + Intent intent = getIntent(); + // FLAG: should just fall back to a standard animation if either: + // 1. images have been added or removed since we've been here, or + // 2. we are currently looking at some image other than the one we + // started on. + + final int totalWidth = mRootView.getMeasuredWidth(); + final int totalHeight = mRootView.getMeasuredHeight(); + + // We need to take a full screen image, and scale/translate it so that + // it appears at exactly the same location onscreen as it is in the + // prior activity. + // The final image will take either the full screen width or height (or both). + final float scaleW = (float) mAnimationStartWidth / totalWidth; + final float scaleY = (float) mAnimationStartHeight / totalHeight; + final float scale = Math.max(scaleW, scaleY); + + final int translateX = calculateTranslate(mAnimationStartX, mAnimationStartWidth, + totalWidth, scale); + final int translateY = calculateTranslate(mAnimationStartY, mAnimationStartHeight, + totalHeight, scale); + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + mBackground.animate().alpha(0f).setDuration(EXIT_ANIMATION_DURATION_MS).start(); + mBackground.setVisibility(View.VISIBLE); + + Runnable endRunnable = new Runnable() { + @Override + public void run() { + PhotoViewActivity.this.onExitAnimationComplete(); + } + }; + // If the temporary image is still visible it means that we have + // not yet loaded the fullres image, so we need to animate + // the temporary image out. + ViewPropertyAnimator animator = null; + if (mTemporaryImage.getVisibility() == View.VISIBLE) { + animator = mTemporaryImage.animate().scaleX(scale).scaleY(scale) + .translationX(translateX).translationY(translateY) + .setDuration(EXIT_ANIMATION_DURATION_MS); + } else { + animator = mViewPager.animate().scaleX(scale).scaleY(scale) + .translationX(translateX).translationY(translateY) + .setDuration(EXIT_ANIMATION_DURATION_MS); + } + if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + animator.withEndAction(endRunnable); + } else { + mHandler.postDelayed(endRunnable, EXIT_ANIMATION_DURATION_MS); + } + animator.start(); + } else { + final Animation alphaAnimation = new AlphaAnimation(1f, 0f); + alphaAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); + mBackground.startAnimation(alphaAnimation); + mBackground.setVisibility(View.VISIBLE); + + final Animation scaleAnimation = new ScaleAnimation(1f, 1f, scale, scale); + scaleAnimation.setDuration(EXIT_ANIMATION_DURATION_MS); + AnimationListener listener = new AnimationListener() { + @Override + public void onAnimationEnd(Animation arg0) { + PhotoViewActivity.this.onExitAnimationComplete(); + } + + @Override + public void onAnimationRepeat(Animation arg0) { + } + + @Override + public void onAnimationStart(Animation arg0) { + } + }; + scaleAnimation.setAnimationListener(listener); + // If the temporary image is still visible it means that we have + // not yet loaded the fullres image, so we need to animate + // the temporary image out. + if (mTemporaryImage.getVisibility() == View.VISIBLE) { + mTemporaryImage.startAnimation(scaleAnimation); + } else { + mViewPager.startAnimation(scaleAnimation); + } + } + } + + private int calculateTranslate(int start, int startSize, int totalSize, float scale) { + // Translation takes precedence over scale. What this means is that if + // we want an view's upper left corner to be a particular spot on screen, + // but that view is scaled to something other than 1, we need to take into + // account the pixels lost to scaling. + // So if we have a view that is 200x300, and we want it's upper left corner + // to be at 50x50, but it's scaled by 50%, we can't just translate it to 50x50. + // If we were to do that, the view's *visible* upper left corner would be at + // 100x200. We need to take into account the difference between the outside + // size of the view (i.e. the size prior to scaling) and the scaled size. + // scaleFromEdge is the difference between the visible left edge and the + // actual left edge, due to scaling. + // scaleFromTop is the difference between the visible top edge, and the + // actual top edge, due to scaling. + int scaleFromEdge = Math.round((totalSize - totalSize * scale) / 2); + + // The imageView is fullscreen, regardless of the aspect ratio of the actual image. + // This means that some portion of the imageView will be blank. We need to + // take into account the size of the blank area so that the actual image + // lines up with the starting image. + int blankSize = Math.round((totalSize * scale - startSize) / 2); + + return start - scaleFromEdge - blankSize; + } + + private void initTemporaryImage(Bitmap bitmap) { + if (mEnterAnimationFinished) { + // Forget this, we've already run the animation. + return; + } + mTemporaryImage.setImageBitmap(bitmap); + if (bitmap != null) { + // We have not yet run the enter animation. Start it now. + int totalWidth = mRootView.getMeasuredWidth(); + if (totalWidth == 0) { + // the measure pass has not yet finished. We can't properly + // run out animation until that is done. Listen for the layout + // to occur, then fire the animation. + final View base = mRootView; + base.getViewTreeObserver().addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + int version = android.os.Build.VERSION.SDK_INT; + if (version >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + base.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } else { + base.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + runEnterAnimation(); + } + }); + } else { + // initiate the animation + runEnterAnimation(); + } + } + // Kick off the photo list loader + getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this); + } + + private class BitmapCallback implements LoaderManager.LoaderCallbacks<BitmapResult> { + + @Override + public Loader<BitmapResult> onCreateLoader(int id, Bundle args) { + String uri = args.getString(ARG_IMAGE_URI); + switch (id) { + case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: + return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, + args, uri); + case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: + return onCreateBitmapLoader(PhotoViewCallbacks.BITMAP_LOADER_AVATAR, + args, uri); + } + return null; + } + + @Override + public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) { + Bitmap bitmap = result.bitmap; + final ActionBar actionBar = getSupportActionBar(); + switch (loader.getId()) { + case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: + // We just loaded the initial thumbnail that we can display + // while waiting for the full viewPager to get initialized. + initTemporaryImage(bitmap); + // Destroy the loader so we don't attempt to load the thumbnail + // again on screen rotations. + getSupportLoaderManager().destroyLoader( + PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL); + break; + case PhotoViewCallbacks.BITMAP_LOADER_AVATAR: + if (bitmap == null) { + actionBar.setLogo(null); + } else { + BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); + actionBar.setLogo(drawable); + } + break; + } + } + + @Override + public void onLoaderReset(Loader<BitmapResult> loader) { + // Do nothing + } + } +} diff --git a/res/drawable-hdpi/actionbar_translucent.9.png b/res/drawable-hdpi/actionbar_translucent.9.png Binary files differindex f18761f..4b40967 100644 --- a/res/drawable-hdpi/actionbar_translucent.9.png +++ b/res/drawable-hdpi/actionbar_translucent.9.png diff --git a/res/drawable-mdpi/actionbar_translucent.9.png b/res/drawable-mdpi/actionbar_translucent.9.png Binary files differindex f78fb8a..a995d44 100644 --- a/res/drawable-mdpi/actionbar_translucent.9.png +++ b/res/drawable-mdpi/actionbar_translucent.9.png diff --git a/res/drawable-xhdpi/actionbar_translucent.9.png b/res/drawable-xhdpi/actionbar_translucent.9.png Binary files differnew file mode 100644 index 0000000..f4ed5fa --- /dev/null +++ b/res/drawable-xhdpi/actionbar_translucent.9.png diff --git a/res/drawable-xxhdpi/actionbar_translucent.9.png b/res/drawable-xxhdpi/actionbar_translucent.9.png Binary files differnew file mode 100644 index 0000000..13e4dbc --- /dev/null +++ b/res/drawable-xxhdpi/actionbar_translucent.9.png diff --git a/res/drawable-xxhdpi/ic_menu_refresh_holo_dark.png b/res/drawable-xxhdpi/ic_menu_refresh_holo_dark.png Binary files differnew file mode 100644 index 0000000..088d76b --- /dev/null +++ b/res/drawable-xxhdpi/ic_menu_refresh_holo_dark.png diff --git a/res/layout-v11/photo_retry_button.xml b/res/layout-v11/photo_retry_button.xml new file mode 100644 index 0000000..c7c7d64 --- /dev/null +++ b/res/layout-v11/photo_retry_button.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<ImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/retry_button" + android:layout_width="@dimen/retry_button_size" + android:layout_height="@dimen/retry_button_size" + android:layout_below="@id/empty_text" + android:layout_centerHorizontal="true" + android:background="?android:attr/selectableItemBackground" + android:scaleType="center" + android:src="@drawable/ic_menu_refresh_holo_dark" + android:visibility="gone" /> diff --git a/res/layout/photo_activity_view.xml b/res/layout/photo_activity_view.xml index e273140..67f505d 100644 --- a/res/layout/photo_activity_view.xml +++ b/res/layout/photo_activity_view.xml @@ -20,6 +20,19 @@ android:layout_width="match_parent" android:layout_height="match_parent" > + <View + android:id="@+id/photo_activity_background" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#ff000000" /> + + <ImageView + android:id="@+id/photo_activity_temporary_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone"/> + <com.android.ex.photo.PhotoViewPager android:id="@+id/photo_view_pager" android:layout_width="match_parent" diff --git a/res/layout/photo_fragment_view.xml b/res/layout/photo_fragment_view.xml index 3dea9f1..9fcd6ee 100644 --- a/res/layout/photo_fragment_view.xml +++ b/res/layout/photo_fragment_view.xml @@ -68,15 +68,6 @@ android:textColor="@android:color/white" android:visibility="gone" /> - <ImageView - android:id="@+id/retry_button" - android:layout_width="@dimen/retry_button_size" - android:layout_height="@dimen/retry_button_size" - android:layout_below="@id/empty_text" - android:layout_centerHorizontal="true" - android:background="?android:attr/selectableItemBackground" - android:scaleType="center" - android:src="@drawable/ic_menu_refresh_holo_dark" - android:visibility="gone" /> - + <!-- retry button might use a background that is ICS+ so include it from the right layout --> + <include layout="@layout/photo_retry_button" /> </RelativeLayout> diff --git a/res/layout/photo_retry_button.xml b/res/layout/photo_retry_button.xml new file mode 100644 index 0000000..29a2508 --- /dev/null +++ b/res/layout/photo_retry_button.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<ImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/retry_button" + android:layout_width="@dimen/retry_button_size" + android:layout_height="@dimen/retry_button_size" + android:layout_below="@id/empty_text" + android:layout_centerHorizontal="true" + android:scaleType="center" + android:src="@drawable/ic_menu_refresh_holo_dark" + android:visibility="gone" /> diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml new file mode 100644 index 0000000..880c1c3 --- /dev/null +++ b/res/values-en-rIN/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="CURRENT_POS">%d</xliff:g> of <xliff:g id="COUNT">%d</xliff:g>"</string> + <string name="retry" msgid="3319517143224679074">"Retry"</string> + <string name="failed" msgid="1458877219699376279">"Couldn\'t load image"</string> +</resources> diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml new file mode 100644 index 0000000..d87b0e2 --- /dev/null +++ b/res/values-et-rEE/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="CURRENT_POS">%d</xliff:g>/<xliff:g id="COUNT">%d</xliff:g>"</string> + <string name="retry" msgid="3319517143224679074">"Proovi uuesti"</string> + <string name="failed" msgid="1458877219699376279">"Kujutist ei õnnestunud laadida"</string> +</resources> diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml new file mode 100644 index 0000000..2e004f0 --- /dev/null +++ b/res/values-fr-rCA/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="CURRENT_POS">%d</xliff:g> sur <xliff:g id="COUNT">%d</xliff:g>"</string> + <string name="retry" msgid="3319517143224679074">"Réessayer"</string> + <string name="failed" msgid="1458877219699376279">"Impossible de charger l\'image."</string> +</resources> diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000..b111dc9 --- /dev/null +++ b/res/values-hy-rAM/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="COUNT">%d</xliff:g>-ից <xliff:g id="CURRENT_POS">%d</xliff:g>-ը"</string> + <string name="retry" msgid="3319517143224679074">"Կրկին փորձել"</string> + <string name="failed" msgid="1458877219699376279">"Չհաջողվեց բեռնել պատկերը"</string> +</resources> diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml new file mode 100644 index 0000000..cb278b3 --- /dev/null +++ b/res/values-ka-rGE/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="COUNT">%d</xliff:g>-ის <xliff:g id="CURRENT_POS">%d</xliff:g>"</string> + <string name="retry" msgid="3319517143224679074">"გამეორება"</string> + <string name="failed" msgid="1458877219699376279">"სურათი ვერ ჩაიტვირთა"</string> +</resources> diff --git a/res/values-km-rKH/strings.xml b/res/values-km-rKH/strings.xml new file mode 100644 index 0000000..3129bd0 --- /dev/null +++ b/res/values-km-rKH/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="CURRENT_POS">%d</xliff:g> នៃ <xliff:g id="COUNT">%d</xliff:g>"</string> + <string name="retry" msgid="3319517143224679074">"ព្យាយាមម្ដងទៀត"</string> + <string name="failed" msgid="1458877219699376279">"មិនអាចផ្ទុករូបភាព"</string> +</resources> diff --git a/res/values-lo-rLA/strings.xml b/res/values-lo-rLA/strings.xml new file mode 100644 index 0000000..50d0809 --- /dev/null +++ b/res/values-lo-rLA/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="CURRENT_POS">%d</xliff:g> ຈາກທັງໝົດ <xliff:g id="COUNT">%d</xliff:g>"</string> + <string name="retry" msgid="3319517143224679074">"ລອງໃໝ່"</string> + <string name="failed" msgid="1458877219699376279">"ບໍ່ສາມາດໂຫຼດຮູບໄດ້"</string> +</resources> diff --git a/res/values-mn-rMN/strings.xml b/res/values-mn-rMN/strings.xml new file mode 100644 index 0000000..4e49795 --- /dev/null +++ b/res/values-mn-rMN/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="COUNT">%d</xliff:g>-н <xliff:g id="CURRENT_POS">%d</xliff:g>"</string> + <string name="retry" msgid="3319517143224679074">"Дахих"</string> + <string name="failed" msgid="1458877219699376279">"Зургийг ачааллаж чадсангүй"</string> +</resources> diff --git a/res/values-ms-rMY/strings.xml b/res/values-ms-rMY/strings.xml new file mode 100644 index 0000000..fa1c78c --- /dev/null +++ b/res/values-ms-rMY/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"<xliff:g id="CURRENT_POS">%d</xliff:g> daripada <xliff:g id="COUNT">%d</xliff:g>"</string> + <string name="retry" msgid="3319517143224679074">"Cuba semula"</string> + <string name="failed" msgid="1458877219699376279">"Tidak dapat memuatkan imej"</string> +</resources> diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000..9846e88 --- /dev/null +++ b/res/values-zh-rHK/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photo_view_count" msgid="3466690572899800275">"第 <xliff:g id="CURRENT_POS">%d</xliff:g> 張 (共 <xliff:g id="COUNT">%d</xliff:g> 張)"</string> + <string name="retry" msgid="3319517143224679074">"重試"</string> + <string name="failed" msgid="1458877219699376279">"無法載入相片"</string> +</resources> diff --git a/sample/Android.mk b/sample/Android.mk index 96607cc..563b312 100644 --- a/sample/Android.mk +++ b/sample/Android.mk @@ -15,8 +15,8 @@ LOCAL_PATH:= $(call my-dir) #Include res dir from photoviewer -photo_dir := ..//res -res_dirs := $(photo_dir) res +photo_dir := ../res ../appcompat/res +res_dirs := $(photo_dir) res ../../../support/v7/appcompat/res ################################################## # Build APK @@ -25,18 +25,18 @@ include $(CLEAR_VARS) src_dirs := src LOCAL_PACKAGE_NAME := LibPhotoViewerSample -LOCAL_STATIC_JAVA_LIBRARIES += libphotoviewer +LOCAL_STATIC_JAVA_LIBRARIES += libphotoviewer_appcompat LOCAL_STATIC_JAVA_LIBRARIES += android-common LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4 LOCAL_STATIC_JAVA_LIBRARIES += android-support-v13 -LOCAL_SDK_VERSION := 16 +LOCAL_SDK_VERSION := current LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs)) \ $(call all-logtags-files-under, $(src_dirs)) -LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) $(LOCAL_PATH)/res +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) LOCAL_AAPT_FLAGS := --auto-add-overlay -LOCAL_AAPT_FLAGS += --extra-packages com.android.ex.photo +LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.appcompat:com.android.ex.photo include $(BUILD_PACKAGE) diff --git a/sample/AndroidManifest.xml b/sample/AndroidManifest.xml index 8a75d6a..edbb41e 100644 --- a/sample/AndroidManifest.xml +++ b/sample/AndroidManifest.xml @@ -4,8 +4,7 @@ android:versionName="1.0" > <uses-sdk - android:minSdkVersion="14" - android:targetSdkVersion="16" /> + android:minSdkVersion="14"/> <application android:icon="@drawable/ic_launcher" diff --git a/sample/res/menu/activity_main.xml b/sample/res/menu/activity_main.xml index cfc10fd..c1bc141 100644 --- a/sample/res/menu/activity_main.xml +++ b/sample/res/menu/activity_main.xml @@ -1,6 +1,6 @@ -<menu xmlns:android="http://schemas.android.com/apk/res/android"> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/menu_settings" android:title="@string/menu_settings" - android:orderInCategory="100" - android:showAsAction="never" /> + app:showAsAction="never" /> </menu> diff --git a/sample/res/values-en-rIN/strings.xml b/sample/res/values-en-rIN/strings.xml new file mode 100644 index 0000000..66a0898 --- /dev/null +++ b/sample/res/values-en-rIN/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"PhotoViewerSample"</string> + <string name="menu_settings" msgid="1259682084875185697">"Settings"</string> + <string name="title_activity_main" msgid="7695239211427024237">"MainActivity"</string> + <string name="launch" msgid="2345080120370773520">"Launch Photo Viewer"</string> +</resources> diff --git a/sample/res/values-et-rEE/strings.xml b/sample/res/values-et-rEE/strings.xml new file mode 100644 index 0000000..9532a25 --- /dev/null +++ b/sample/res/values-et-rEE/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"PhotoViewerSample"</string> + <string name="menu_settings" msgid="1259682084875185697">"Seaded"</string> + <string name="title_activity_main" msgid="7695239211427024237">"MainActivity"</string> + <string name="launch" msgid="2345080120370773520">"Fotovaaturi käivitamine"</string> +</resources> diff --git a/sample/res/values-fr-rCA/strings.xml b/sample/res/values-fr-rCA/strings.xml new file mode 100644 index 0000000..6e3cf79 --- /dev/null +++ b/sample/res/values-fr-rCA/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"PhotoViewerSample"</string> + <string name="menu_settings" msgid="1259682084875185697">"Paramètres"</string> + <string name="title_activity_main" msgid="7695239211427024237">"MainActivity"</string> + <string name="launch" msgid="2345080120370773520">"Lancer la visionneuse de photos"</string> +</resources> diff --git a/sample/res/values-hy-rAM/strings.xml b/sample/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000..e2f3278 --- /dev/null +++ b/sample/res/values-hy-rAM/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"PhotoViewerSample"</string> + <string name="menu_settings" msgid="1259682084875185697">"Կարգավորումներ"</string> + <string name="title_activity_main" msgid="7695239211427024237">"MainActivity"</string> + <string name="launch" msgid="2345080120370773520">"Գործարկել Լուսանկարի դիտարկիչը"</string> +</resources> diff --git a/sample/res/values-ka-rGE/strings.xml b/sample/res/values-ka-rGE/strings.xml new file mode 100644 index 0000000..c0b3e6c --- /dev/null +++ b/sample/res/values-ka-rGE/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"ფოტოდათვალიერების ნიმუში"</string> + <string name="menu_settings" msgid="1259682084875185697">"პარამეტრები"</string> + <string name="title_activity_main" msgid="7695239211427024237">"ძირითადი აქტივობა"</string> + <string name="launch" msgid="2345080120370773520">"Photo Viewer-ის გაშვება"</string> +</resources> diff --git a/sample/res/values-km-rKH/strings.xml b/sample/res/values-km-rKH/strings.xml new file mode 100644 index 0000000..d93bb8f --- /dev/null +++ b/sample/res/values-km-rKH/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"គំរូកម្មវិធីមើលរូបថត"</string> + <string name="menu_settings" msgid="1259682084875185697">"ការកំណត់"</string> + <string name="title_activity_main" msgid="7695239211427024237">"សកម្មភាពមេ"</string> + <string name="launch" msgid="2345080120370773520">"ចាប់ផ្ដើមកម្មវិធីបង្ហាញរូបថត"</string> +</resources> diff --git a/sample/res/values-lo-rLA/strings.xml b/sample/res/values-lo-rLA/strings.xml new file mode 100644 index 0000000..5b10697 --- /dev/null +++ b/sample/res/values-lo-rLA/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"PhotoViewerSample"</string> + <string name="menu_settings" msgid="1259682084875185697">"ການຕັ້ງຄ່າ"</string> + <string name="title_activity_main" msgid="7695239211427024237">"MainActivity"</string> + <string name="launch" msgid="2345080120370773520">"ເປີດໂປຣແກຣມເບິ່ງຮູບ"</string> +</resources> diff --git a/sample/res/values-mn-rMN/strings.xml b/sample/res/values-mn-rMN/strings.xml new file mode 100644 index 0000000..d2b13ae --- /dev/null +++ b/sample/res/values-mn-rMN/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"ЗурагХаруулагчЖишээ"</string> + <string name="menu_settings" msgid="1259682084875185697">"Тохиргоо"</string> + <string name="title_activity_main" msgid="7695239211427024237">"ҮндсэнАктивити"</string> + <string name="launch" msgid="2345080120370773520">"Зураг харагч эхлүүлэх"</string> +</resources> diff --git a/sample/res/values-ms-rMY/strings.xml b/sample/res/values-ms-rMY/strings.xml new file mode 100644 index 0000000..8ad0bca --- /dev/null +++ b/sample/res/values-ms-rMY/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"PhotoViewerSample"</string> + <string name="menu_settings" msgid="1259682084875185697">"Tetapan"</string> + <string name="title_activity_main" msgid="7695239211427024237">"Aktiviti Utama"</string> + <string name="launch" msgid="2345080120370773520">"Lancarkan Photo Viewer"</string> +</resources> diff --git a/sample/res/values-zh-rHK/strings.xml b/sample/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000..73dd1fa --- /dev/null +++ b/sample/res/values-zh-rHK/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="559800164925277094">"PhotoViewerSample"</string> + <string name="menu_settings" msgid="1259682084875185697">"設定"</string> + <string name="title_activity_main" msgid="7695239211427024237">"MainActivity"</string> + <string name="launch" msgid="2345080120370773520">"啟動相片檢視器"</string> +</resources> diff --git a/src/com/android/ex/photo/Intents.java b/src/com/android/ex/photo/Intents.java index 35bf33f..3c4088c 100644 --- a/src/com/android/ex/photo/Intents.java +++ b/src/com/android/ex/photo/Intents.java @@ -27,6 +27,7 @@ import com.android.ex.photo.fragments.PhotoViewFragment; /** * Build intents to start app activities */ + public class Intents { // Intent extras public static final String EXTRA_PHOTO_INDEX = "photo_index"; @@ -36,6 +37,19 @@ public class Intents { public static final String EXTRA_PROJECTION = "projection"; public static final String EXTRA_THUMBNAIL_URI = "thumbnail_uri"; public static final String EXTRA_MAX_INITIAL_SCALE = "max_scale"; + public static final String EXTRA_WATCH_NETWORK = "watch_network"; + + + // Parameters affecting the intro/exit animation + public static final String EXTRA_SCALE_UP_ANIMATION = "scale_up_animation"; + public static final String EXTRA_ANIMATION_START_X = "start_x_extra"; + public static final String EXTRA_ANIMATION_START_Y = "start_y_extra"; + public static final String EXTRA_ANIMATION_START_WIDTH = "start_width_extra"; + public static final String EXTRA_ANIMATION_START_HEIGHT = "start_height_extra"; + + // Parameters affecting the display and features + public static final String EXTRA_ACTION_BAR_HIDDEN_INITIALLY = "action_bar_hidden_initially"; + public static final String EXTRA_DISPLAY_THUMBS_FULLSCREEN = "display_thumbs_fullscreen"; /** * Gets a photo view intent builder to display the photos from phone activity. @@ -71,6 +85,8 @@ public class Intents { private Integer mPhotoIndex; /** The URI of the initial photo to show */ private String mInitialPhotoUri; + /** The URI of the initial thumbnail to show */ + private String mInitialThumbnailUri; /** The URI of the group of photos to display */ private String mPhotosUri; /** The URL of the photo to display */ @@ -81,9 +97,30 @@ public class Intents { private String mThumbnailUri; /** The maximum scale to display images at before */ private Float mMaxInitialScale; + /** + * True if the PhotoViewFragments should watch for network changes to restart their loaders + */ + private boolean mWatchNetwork; + /** true we want to run the image scale animation */ + private boolean mScaleAnimation; + /** The parameters for performing the scale up/scale down animations + * upon enter and exit. StartX and StartY represent the screen coordinates + * of the upper left corner of the start rectangle, startWidth and startHeight + * represent the width and height of the start rectangle. + */ + private int mStartX; + private int mStartY; + private int mStartWidth; + private int mStartHeight; + + private boolean mActionBarHiddenInitially; + private boolean mDisplayFullScreenThumbs; private PhotoViewIntentBuilder(Context context, Class<?> cls) { mIntent = new Intent(context, cls); + mScaleAnimation = false; + mActionBarHiddenInitially = false; + mDisplayFullScreenThumbs = false; } /** Sets the photo index */ @@ -136,6 +173,49 @@ public class Intents { return this; } + /** + * Enable watching the network for connectivity changes. + * + * When a change is detected, bitmap loaders will be restarted if required. + */ + public PhotoViewIntentBuilder watchNetworkConnectivityChanges() { + mWatchNetwork = true; + return this; + } + + public PhotoViewIntentBuilder setScaleAnimation(int startX, int startY, + int startWidth, int startHeight) { + mScaleAnimation = true; + mStartX = startX; + mStartY = startY; + mStartWidth = startWidth; + mStartHeight = startHeight; + return this; + } + + // If this option is turned on, then the photoViewer will be initially + // displayed with the action bar hidden. This is as opposed to the default + // behavior, where the actionBar is initially shown. + public PhotoViewIntentBuilder setActionBarHiddenInitially( + boolean actionBarHiddenInitially) { + mActionBarHiddenInitially = actionBarHiddenInitially; + return this; + } + + // If this option is turned on, then the small, lo-res thumbnail will + // be scaled up to the maximum size to cover as much of the screen as + // possible while still maintaining the correct aspect ratio. This means + // that the image may appear blurry until the the full-res image is + // loaded. + // This is as opposed to the default behavior, where only part of the + // thumbnail is displayed in a small view in the center of the screen, + // and a loading spinner is displayed until the full-res image is loaded. + public PhotoViewIntentBuilder setDisplayThumbsFullScreen( + boolean displayFullScreenThumbs) { + mDisplayFullScreenThumbs = displayFullScreenThumbs; + return this; + } + /** Build the intent */ public Intent build() { mIntent.setAction(Intent.ACTION_VIEW); @@ -175,6 +255,21 @@ public class Intents { mIntent.putExtra(EXTRA_MAX_INITIAL_SCALE, mMaxInitialScale); } + if (mWatchNetwork == true) { + mIntent.putExtra(EXTRA_WATCH_NETWORK, true); + } + + mIntent.putExtra(EXTRA_SCALE_UP_ANIMATION, mScaleAnimation); + if (mScaleAnimation) { + mIntent.putExtra(EXTRA_ANIMATION_START_X, mStartX); + mIntent.putExtra(EXTRA_ANIMATION_START_Y, mStartY); + mIntent.putExtra(EXTRA_ANIMATION_START_WIDTH, mStartWidth); + mIntent.putExtra(EXTRA_ANIMATION_START_HEIGHT, mStartHeight); + } + + mIntent.putExtra(EXTRA_ACTION_BAR_HIDDEN_INITIALLY, mActionBarHiddenInitially); + mIntent.putExtra(EXTRA_DISPLAY_THUMBS_FULLSCREEN, mDisplayFullScreenThumbs); + return mIntent; } } diff --git a/src/com/android/ex/photo/PhotoViewActivity.java b/src/com/android/ex/photo/PhotoViewActivity.java deleted file mode 100644 index 30bc916..0000000 --- a/src/com/android/ex/photo/PhotoViewActivity.java +++ /dev/null @@ -1,620 +0,0 @@ -/* - * 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<Cursor>, 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<Integer, OnScreenListener> - mScreenListeners = new HashMap<Integer, OnScreenListener>(); - /** The set of listeners wanting full screen state */ - private Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>(); - /** 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<Cursor> 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<Cursor> 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<Cursor> 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; - } -} diff --git a/src/com/android/ex/photo/PhotoViewCallbacks.java b/src/com/android/ex/photo/PhotoViewCallbacks.java index d71c710..e364fd3 100644 --- a/src/com/android/ex/photo/PhotoViewCallbacks.java +++ b/src/com/android/ex/photo/PhotoViewCallbacks.java @@ -1,12 +1,20 @@ package com.android.ex.photo; import android.database.Cursor; +import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.content.Loader; import com.android.ex.photo.adapters.PhotoPagerAdapter; import com.android.ex.photo.fragments.PhotoViewFragment; +import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; public interface PhotoViewCallbacks { + + public static final int BITMAP_LOADER_AVATAR = 1; + public static final int BITMAP_LOADER_THUMBNAIL = 2; + public static final int BITMAP_LOADER_PHOTO = 3; + /** * Listener to be invoked for screen events. */ @@ -63,16 +71,21 @@ public interface PhotoViewCallbacks { public void onNewPhotoLoaded(int position); + public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment, + boolean success); + public void toggleFullScreen(); public boolean isFragmentActive(Fragment fragment); - public void onFragmentVisible(Fragment fragment); + public void onFragmentVisible(PhotoViewFragment fragment); public boolean isFragmentFullScreen(Fragment fragment); public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor); + public Loader<BitmapResult> onCreateBitmapLoader(int id, Bundle args, String uri); + /** * Returns the adapter associated with this activity. */ diff --git a/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java b/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java index cfa06db..a7716cf 100644 --- a/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java +++ b/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java @@ -21,14 +21,14 @@ import android.os.Parcelable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; +import android.support.v4.util.LruCache; import android.support.v4.view.PagerAdapter; import android.util.Log; -import android.util.LruCache; import android.view.View; /** - * NOTE: This is a direct copy of {@link FragmentPagerAdapter} with four very important - * modifications. + * NOTE: This is a direct copy of {@link android.support.v4.app.FragmentPagerAdapter} + * with four very important modifications. * <p> * <ol> * <li>The method {@link #makeFragmentName(int, int)} is declared "protected" diff --git a/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java b/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java index c5183ed..30f9e93 100644 --- a/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java +++ b/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java @@ -18,6 +18,7 @@ package com.android.ex.photo.adapters; import android.content.Context; +import android.content.Intent; import android.database.Cursor; import android.support.v4.app.Fragment; @@ -34,11 +35,14 @@ public class PhotoPagerAdapter extends BaseCursorPagerAdapter { protected int mThumbnailUriIndex; protected int mLoadingIndex; protected final float mMaxScale; + protected boolean mDisplayThumbsFullScreen; public PhotoPagerAdapter( - Context context, android.support.v4.app.FragmentManager fm, Cursor c, float maxScale) { + Context context, android.support.v4.app.FragmentManager fm, Cursor c, + float maxScale, boolean thumbsFullScreen) { super(context, fm, c); mMaxScale = maxScale; + mDisplayThumbsFullScreen = thumbsFullScreen; } @Override @@ -62,6 +66,7 @@ public class PhotoPagerAdapter extends BaseCursorPagerAdapter { builder .setResolvedPhotoUri(photoUri) .setThumbnailUri(thumbnailUri) + .setDisplayThumbsFullScreen(mDisplayThumbsFullScreen) .setMaxInitialScale(mMaxScale); return PhotoViewFragment.newInstance(builder.build(), position, onlyShowSpinner); diff --git a/src/com/android/ex/photo/fragments/PhotoViewFragment.java b/src/com/android/ex/photo/fragments/PhotoViewFragment.java index ef1ef0d..eca0850 100644 --- a/src/com/android/ex/photo/fragments/PhotoViewFragment.java +++ b/src/com/android/ex/photo/fragments/PhotoViewFragment.java @@ -21,7 +21,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Bitmap; import android.net.ConnectivityManager; @@ -40,14 +39,15 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; + import com.android.ex.photo.Intents; import com.android.ex.photo.PhotoViewCallbacks; import com.android.ex.photo.PhotoViewCallbacks.CursorChangedListener; import com.android.ex.photo.PhotoViewCallbacks.OnScreenListener; import com.android.ex.photo.R; import com.android.ex.photo.adapters.PhotoPagerAdapter; -import com.android.ex.photo.loaders.PhotoBitmapLoader; -import com.android.ex.photo.loaders.PhotoBitmapLoader.BitmapResult; +import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; +import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface; import com.android.ex.photo.util.ImageUtils; import com.android.ex.photo.views.PhotoView; import com.android.ex.photo.views.ProgressBarWrapper; @@ -60,6 +60,7 @@ public class PhotoViewFragment extends Fragment implements OnClickListener, OnScreenListener, CursorChangedListener { + /** * Interface for components that are internally scrollable left-to-right. */ @@ -87,13 +88,9 @@ public class PhotoViewFragment extends Fragment implements protected final static String STATE_INTENT_KEY = "com.android.mail.photo.fragments.PhotoViewFragment.INTENT"; - private final static String ARG_INTENT = "arg-intent"; - private final static String ARG_POSITION = "arg-position"; - private final static String ARG_SHOW_SPINNER = "arg-show-spinner"; - - // Loader IDs - protected final static int LOADER_ID_PHOTO = 1; - protected final static int LOADER_ID_THUMBNAIL = 2; + protected final static String ARG_INTENT = "arg-intent"; + protected final static String ARG_POSITION = "arg-position"; + protected final static String ARG_SHOW_SPINNER = "arg-show-spinner"; /** The size of the photo */ public static Integer sPhotoSize; @@ -106,6 +103,8 @@ public class PhotoViewFragment extends Fragment implements protected PhotoViewCallbacks mCallback; protected PhotoPagerAdapter mAdapter; + protected BroadcastReceiver mInternetStateReceiver; + protected PhotoView mPhotoView; protected ImageView mPhotoPreviewImage; protected TextView mEmptyText; @@ -117,6 +116,11 @@ public class PhotoViewFragment extends Fragment implements /** Whether or not the fragment should make the photo full-screen */ protected boolean mFullScreen; + /** + * True if the PhotoViewFragment should watch the network state in order to restart loaders. + */ + protected boolean mWatchNetworkState; + /** Whether or not this fragment will only show the loading spinner */ protected boolean mOnlyShowSpinner; @@ -129,6 +133,9 @@ public class PhotoViewFragment extends Fragment implements /** Whether or not there is currently a connection to the internet */ protected boolean mConnected; + /** Whether or not we can display the thumbnail at fullscreen size */ + protected boolean mDisplayThumbsFullScreen; + /** Public no-arg constructor for allowing the framework to handle orientation changes */ public PhotoViewFragment() { // Do nothing. @@ -163,11 +170,6 @@ public class PhotoViewFragment extends Fragment implements if (mAdapter == null) { throw new IllegalStateException("Callback reported null adapter"); } - - if (hasNetworkStatePermission()) { - getActivity().registerReceiver(new InternetStateBroadcastReceiver(), - new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")); - } // Don't call until we've setup the entire view setViewVisibility(); } @@ -207,6 +209,9 @@ public class PhotoViewFragment extends Fragment implements return; } mIntent = bundle.getParcelable(ARG_INTENT); + mDisplayThumbsFullScreen = mIntent.getBooleanExtra( + Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false); + mPosition = bundle.getInt(ARG_POSITION); mOnlyShowSpinner = bundle.getBoolean(ARG_SHOW_SPINNER); mProgressBarNeeded = true; @@ -221,6 +226,7 @@ public class PhotoViewFragment extends Fragment implements if (mIntent != null) { mResolvedPhotoUri = mIntent.getStringExtra(Intents.EXTRA_RESOLVED_PHOTO_URI); mThumbnailUri = mIntent.getStringExtra(Intents.EXTRA_THUMBNAIL_URI); + mWatchNetworkState = mIntent.getBooleanExtra(Intents.EXTRA_WATCH_NETWORK, false); } } @@ -249,6 +255,9 @@ public class PhotoViewFragment extends Fragment implements mPhotoProgressBar = new ProgressBarWrapper(determinate, indeterminate, true); mEmptyText = (TextView) view.findViewById(R.id.empty_text); mRetryButton = (ImageView) view.findViewById(R.id.retry_button); + + // Don't call until we've setup the entire view + setViewVisibility(); } @Override @@ -257,7 +266,12 @@ public class PhotoViewFragment extends Fragment implements mCallback.addScreenListener(mPosition, this); mCallback.addCursorListener(this); - if (hasNetworkStatePermission()) { + if (mWatchNetworkState) { + if (mInternetStateReceiver == null) { + mInternetStateReceiver = new InternetStateBroadcastReceiver(); + } + getActivity().registerReceiver(mInternetStateReceiver, + new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); ConnectivityManager connectivityManager = (ConnectivityManager) getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); @@ -274,14 +288,23 @@ public class PhotoViewFragment extends Fragment implements mProgressBarNeeded = true; mPhotoPreviewAndProgress.setVisibility(View.VISIBLE); - getLoaderManager().initLoader(LOADER_ID_THUMBNAIL, null, this); - getLoaderManager().initLoader(LOADER_ID_PHOTO, null, this); + getLoaderManager().initLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, + null, this); + + // FLAG: If we are displaying thumbnails at fullscreen size, then we + // could defer the loading of the fullscreen image until the thumbnail + // has finished loading, or even until the user attempts to zoom in. + getLoaderManager().initLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO, + null, this); } } @Override public void onPause() { // Remove listeners + if (mWatchNetworkState) { + getActivity().unregisterReceiver(mInternetStateReceiver); + } mCallback.removeCursorListener(this); mCallback.removeScreenListener(mPosition); resetPhotoView(); @@ -298,6 +321,10 @@ public class PhotoViewFragment extends Fragment implements super.onDestroyView(); } + public String getPhotoUri() { + return mResolvedPhotoUri; + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -312,14 +339,16 @@ public class PhotoViewFragment extends Fragment implements if(mOnlyShowSpinner) { return null; } + String uri = null; switch (id) { - case LOADER_ID_PHOTO: - return new PhotoBitmapLoader(getActivity(), mResolvedPhotoUri); - case LOADER_ID_THUMBNAIL: - return new PhotoBitmapLoader(getActivity(), mThumbnailUri); - default: - return null; + case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: + uri = mThumbnailUri; + break; + case PhotoViewCallbacks.BITMAP_LOADER_PHOTO: + uri = mResolvedPhotoUri; + break; } + return mCallback.onCreateBitmapLoader(id, args, uri); } @Override @@ -332,37 +361,35 @@ public class PhotoViewFragment extends Fragment implements final int id = loader.getId(); switch (id) { - case LOADER_ID_THUMBNAIL: - if (isPhotoBound()) { - // There is need to do anything with the thumbnail - // image, as the full size image is being shown. - return; - } - - if (data == null) { - // no preview, show default - mPhotoPreviewImage.setImageResource(R.drawable.default_image); - mThumbnailShown = false; + case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL: + if (mDisplayThumbsFullScreen) { + displayPhoto(result); } else { - // show preview - mPhotoPreviewImage.setImageBitmap(data); - mThumbnailShown = true; + if (isPhotoBound()) { + // There is need to do anything with the thumbnail + // image, as the full size image is being shown. + return; + } + + if (data == null) { + // no preview, show default + mPhotoPreviewImage.setImageResource(R.drawable.default_image); + mThumbnailShown = false; + } else { + // show preview + mPhotoPreviewImage.setImageBitmap(data); + mThumbnailShown = true; + } + mPhotoPreviewImage.setVisibility(View.VISIBLE); + if (getResources().getBoolean(R.bool.force_thumbnail_no_scaling)) { + mPhotoPreviewImage.setScaleType(ImageView.ScaleType.CENTER); + } + enableImageTransforms(false); } - mPhotoPreviewImage.setVisibility(View.VISIBLE); - if (getResources().getBoolean(R.bool.force_thumbnail_no_scaling)) { - mPhotoPreviewImage.setScaleType(ImageView.ScaleType.CENTER); - } - enableImageTransforms(false); break; - case LOADER_ID_PHOTO: - if (result.status == BitmapResult.STATUS_EXCEPTION) { - mProgressBarNeeded = false; - mEmptyText.setText(R.string.failed); - mEmptyText.setVisibility(View.VISIBLE); - } else { - bindPhoto(data); - } + case PhotoViewCallbacks.BITMAP_LOADER_PHOTO: + displayPhoto(result); break; default: break; @@ -379,6 +406,19 @@ public class PhotoViewFragment extends Fragment implements setViewVisibility(); } + private void displayPhoto(BitmapResult result) { + Bitmap data = result.bitmap; + if (result.status == BitmapResult.STATUS_EXCEPTION) { + mProgressBarNeeded = false; + mEmptyText.setText(R.string.failed); + mEmptyText.setVisibility(View.VISIBLE); + mCallback.onFragmentPhotoLoadComplete(this, false /* success */); + } else { + bindPhoto(data); + mCallback.onFragmentPhotoLoadComplete(this, true /* success */); + } + } + /** * Binds an image to the photo view. */ @@ -393,12 +433,6 @@ public class PhotoViewFragment extends Fragment implements } } - private boolean hasNetworkStatePermission() { - final String networkStatePermission = "android.permission.ACCESS_NETWORK_STATE"; - int result = getActivity().checkCallingOrSelfPermission(networkStatePermission); - return result == PackageManager.PERMISSION_GRANTED; - } - /** * Enable or disable image transformations. When transformations are enabled, this view * consumes all touch events. @@ -439,7 +473,8 @@ public class PhotoViewFragment extends Fragment implements } else { if (!isPhotoBound()) { // Restart the loader - getLoaderManager().restartLoader(LOADER_ID_THUMBNAIL, null, this); + getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, + null, this); } mCallback.onFragmentVisible(this); } @@ -504,13 +539,19 @@ public class PhotoViewFragment extends Fragment implements // change. return; } + // FLAG: There is a problem here: + // If the cursor changes, and new items are added at an earlier position than + // the current item, we will switch photos here. Really we should probably + // try to find a photo with the same url and move the cursor to that position. if (cursor.moveToPosition(mPosition) && !isPhotoBound()) { mCallback.onCursorChanged(this, cursor); final LoaderManager manager = getLoaderManager(); - final Loader<BitmapResult> fakePhotoLoader = manager.getLoader(LOADER_ID_PHOTO); + + final Loader<BitmapResult> fakePhotoLoader = manager.getLoader( + PhotoViewCallbacks.BITMAP_LOADER_PHOTO); if (fakePhotoLoader != null) { - final PhotoBitmapLoader loader = (PhotoBitmapLoader) fakePhotoLoader; + final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakePhotoLoader; mResolvedPhotoUri = mAdapter.getPhotoUri(cursor); loader.setPhotoUri(mResolvedPhotoUri); loader.forceLoad(); @@ -518,9 +559,9 @@ public class PhotoViewFragment extends Fragment implements if (!mThumbnailShown) { final Loader<BitmapResult> fakeThumbnailLoader = manager.getLoader( - LOADER_ID_THUMBNAIL); + PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL); if (fakeThumbnailLoader != null) { - final PhotoBitmapLoader loader = (PhotoBitmapLoader) fakeThumbnailLoader; + final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakeThumbnailLoader; mThumbnailUri = mAdapter.getThumbnailUri(cursor); loader.setPhotoUri(mThumbnailUri); loader.forceLoad(); @@ -529,6 +570,10 @@ public class PhotoViewFragment extends Fragment implements } } + public int getPosition() { + return mPosition; + } + public ProgressBarWrapper getPhotoProgressBar() { return mPhotoProgressBar; } @@ -553,16 +598,17 @@ public class PhotoViewFragment extends Fragment implements ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); - if (activeNetInfo == null) { + if (activeNetInfo == null || !activeNetInfo.isConnected()) { mConnected = false; return; } - if (mConnected == false && activeNetInfo.isConnected() && !isPhotoBound()) { + if (mConnected == false && !isPhotoBound()) { if (mThumbnailShown == false) { - getLoaderManager().restartLoader(LOADER_ID_THUMBNAIL, null, - PhotoViewFragment.this); + getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL, + null, PhotoViewFragment.this); } - getLoaderManager().restartLoader(LOADER_ID_PHOTO, null, PhotoViewFragment.this); + getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO, + null, PhotoViewFragment.this); mConnected = true; mPhotoProgressBar.setVisibility(View.VISIBLE); } diff --git a/src/com/android/ex/photo/loaders/PhotoBitmapLoader.java b/src/com/android/ex/photo/loaders/PhotoBitmapLoader.java index ca92050..ace4d85 100644 --- a/src/com/android/ex/photo/loaders/PhotoBitmapLoader.java +++ b/src/com/android/ex/photo/loaders/PhotoBitmapLoader.java @@ -25,13 +25,14 @@ import android.support.v4.content.AsyncTaskLoader; import android.util.DisplayMetrics; import com.android.ex.photo.fragments.PhotoViewFragment; -import com.android.ex.photo.loaders.PhotoBitmapLoader.BitmapResult; +import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; import com.android.ex.photo.util.ImageUtils; /** * Loader for the bitmap of a photo. */ -public class PhotoBitmapLoader extends AsyncTaskLoader<BitmapResult> { +public class PhotoBitmapLoader extends AsyncTaskLoader<BitmapResult> + implements PhotoBitmapLoaderInterface { private String mPhotoUri; private Bitmap mBitmap; @@ -40,6 +41,7 @@ public class PhotoBitmapLoader extends AsyncTaskLoader<BitmapResult> { mPhotoUri = photoUri; } + @Override public void setPhotoUri(String photoUri) { mPhotoUri = photoUri; } @@ -79,6 +81,7 @@ public class PhotoBitmapLoader extends AsyncTaskLoader<BitmapResult> { if (bitmap != null) { onReleaseResources(bitmap); } + return; } Bitmap oldBitmap = mBitmap; mBitmap = bitmap; @@ -167,12 +170,4 @@ public class PhotoBitmapLoader extends AsyncTaskLoader<BitmapResult> { bitmap.recycle(); } } - - public static class BitmapResult { - public static final int STATUS_SUCCESS = 0; - public static final int STATUS_EXCEPTION = 1; - - public Bitmap bitmap; - public int status; - } } diff --git a/src/com/android/ex/photo/loaders/PhotoBitmapLoaderInterface.java b/src/com/android/ex/photo/loaders/PhotoBitmapLoaderInterface.java new file mode 100644 index 0000000..4060b55 --- /dev/null +++ b/src/com/android/ex/photo/loaders/PhotoBitmapLoaderInterface.java @@ -0,0 +1,18 @@ +package com.android.ex.photo.loaders; + +import android.graphics.Bitmap; + +public interface PhotoBitmapLoaderInterface { + + public void setPhotoUri(String photoUri); + + public void forceLoad(); + + public static class BitmapResult { + public static final int STATUS_SUCCESS = 0; + public static final int STATUS_EXCEPTION = 1; + + public Bitmap bitmap; + public int status; + } +} diff --git a/src/com/android/ex/photo/provider/PhotoContract.java b/src/com/android/ex/photo/provider/PhotoContract.java index 8483620..c7e49fd 100644 --- a/src/com/android/ex/photo/provider/PhotoContract.java +++ b/src/com/android/ex/photo/provider/PhotoContract.java @@ -49,11 +49,10 @@ public final class PhotoContract { */ public static final String CONTENT_TYPE = "contentType"; /** - * This boolean column indicates that a loading indicator should display permenantly + * This boolean column indicates that a loading indicator should display permanently * if no image urls are provided. */ public static final String LOADING_INDICATOR = "loadingIndicator"; - } public static interface PhotoQuery { @@ -63,7 +62,7 @@ public final class PhotoContract { PhotoViewColumns.NAME, PhotoViewColumns.CONTENT_URI, PhotoViewColumns.THUMBNAIL_URI, - PhotoViewColumns.CONTENT_TYPE, + PhotoViewColumns.CONTENT_TYPE }; } diff --git a/src/com/android/ex/photo/util/Exif.java b/src/com/android/ex/photo/util/Exif.java index 743b896..9d0c283 100644 --- a/src/com/android/ex/photo/util/Exif.java +++ b/src/com/android/ex/photo/util/Exif.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2013 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. @@ -18,21 +18,68 @@ package com.android.ex.photo.util; import android.util.Log; +import java.io.ByteArrayInputStream; +import java.io.InputStream; + public class Exif { private static final String TAG = "CameraExif"; - // Returns the degrees in clockwise. Values are 0, 90, 180, or 270. - public static int getOrientation(byte[] jpeg) { - if (jpeg == null) { + /** + * Returns the degrees in clockwise. Values are 0, 90, 180, or 270. + * @param inputStream The input stream will not be closed for you. + * @param byteSize Recommended parameter declaring the length of the input stream. If you + * pass in -1, we will have to read more from the input stream. + * @return 0, 90, 180, or 270. + */ + public static int getOrientation(final InputStream inputStream, final long byteSize) { + if (inputStream == null) { return 0; } + /* + Looking at this algorithm, we never look ahead more than 8 bytes. As long as we call + advanceTo() at the end of every loop, we should never have to reallocate a larger buffer. + + Also, the most we ever read backwards is 4 bytes. pack() reads backwards if the encoding + is in little endian format. These following two lines potentially reads 4 bytes backwards: + + int tag = pack(jpeg, offset, 4, false); + count = pack(jpeg, offset - 2, 2, littleEndian); + + To be safe, we will always advance to some index-4, so we'll need 4 more for the +8 + look ahead, which makes it a +12 look ahead total. Use 16 just in case my analysis is off. + + This means we only need to allocate a single 16 byte buffer. + + Note: If you do not pass in byteSize parameter, a single large allocation will occur. + For a 1MB image, I see one 30KB allocation. This is due to the line containing: + + has(jpeg, byteSize, offset + length - 1) + + where length is a variable int (around 30KB above) read from the EXIF headers. + + This is still much better than allocating a 1MB byte[] which we were doing before. + */ + + final int lookAhead = 16; + final int readBackwards = 4; + final InputStreamBuffer jpeg = new InputStreamBuffer(inputStream, lookAhead, false); + int offset = 0; int length = 0; + if (has(jpeg, byteSize, 1)) { + // JPEG image files begin with FF D8. Only JPEG images have EXIF data. + final boolean possibleJpegFormat = jpeg.get(0) == (byte) 0xFF + && jpeg.get(1) == (byte) 0xD8; + if (!possibleJpegFormat) { + return 0; + } + } + // ISO/IEC 10918-1:1993(E) - while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) { - int marker = jpeg[offset] & 0xFF; + while (has(jpeg, byteSize, offset + 3) && (jpeg.get(offset++) & 0xFF) == 0xFF) { + final int marker = jpeg.get(offset) & 0xFF; // Check if the marker is a padding. if (marker == 0xFF) { @@ -46,12 +93,14 @@ public class Exif { } // Check if the marker is EOI or SOS. if (marker == 0xD9 || marker == 0xDA) { + // Loop ends. + jpeg.advanceTo(offset - readBackwards); break; } // Get the length and check if it is reasonable. length = pack(jpeg, offset, 2, false); - if (length < 2 || offset + length > jpeg.length) { + if (length < 2 || !has(jpeg, byteSize, offset + length - 1)) { Log.e(TAG, "Invalid length"); return 0; } @@ -62,12 +111,17 @@ public class Exif { pack(jpeg, offset + 6, 2, false) == 0) { offset += 8; length -= 8; + // Loop ends. + jpeg.advanceTo(offset - readBackwards); break; } // Skip other markers. offset += length; length = 0; + + // Loop ends. + jpeg.advanceTo(offset - readBackwards); } // JEITA CP-3451 Exif Version 2.2 @@ -78,7 +132,7 @@ public class Exif { Log.e(TAG, "Invalid byte order"); return 0; } - boolean littleEndian = (tag == 0x49492A00); + final boolean littleEndian = (tag == 0x49492A00); // Get the offset and check if it is reasonable. int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; @@ -89,14 +143,18 @@ public class Exif { offset += count; length -= count; + // Offset has changed significantly. + jpeg.advanceTo(offset - readBackwards); + // Get the count and go through all the elements. count = pack(jpeg, offset - 2, 2, littleEndian); + while (count-- > 0 && length >= 12) { // Get the tag and check if it is orientation. tag = pack(jpeg, offset, 2, littleEndian); if (tag == 0x0112) { // We do not really care about type and count, do we? - int orientation = pack(jpeg, offset + 8, 2, littleEndian); + final int orientation = pack(jpeg, offset + 8, 2, littleEndian); switch (orientation) { case 1: return 0; @@ -112,15 +170,17 @@ public class Exif { } offset += 12; length -= 12; + + // Loop ends. + jpeg.advanceTo(offset - readBackwards); } } - Log.i(TAG, "Orientation not found"); return 0; } - private static int pack(byte[] bytes, int offset, int length, - boolean littleEndian) { + private static int pack(final InputStreamBuffer bytes, int offset, int length, + final boolean littleEndian) { int step = 1; if (littleEndian) { offset += length - 1; @@ -129,9 +189,23 @@ public class Exif { int value = 0; while (length-- > 0) { - value = (value << 8) | (bytes[offset] & 0xFF); + value = (value << 8) | (bytes.get(offset) & 0xFF); offset += step; } return value; } + + private static boolean has(final InputStreamBuffer jpeg, final long byteSize, final int index) { + if (byteSize >= 0) { + return index < byteSize; + } else { + // For large values of index, this will cause the internal buffer to resize. + return jpeg.has(index); + } + } + + @Deprecated + public static int getOrientation(final byte[] jpeg) { + return getOrientation(new ByteArrayInputStream(jpeg), jpeg.length); + } } diff --git a/src/com/android/ex/photo/util/ImageUtils.java b/src/com/android/ex/photo/util/ImageUtils.java index 0db064e..43c4510 100644 --- a/src/com/android/ex/photo/util/ImageUtils.java +++ b/src/com/android/ex/photo/util/ImageUtils.java @@ -29,7 +29,7 @@ import android.util.Base64; import android.util.Log; import com.android.ex.photo.PhotoViewActivity; -import com.android.ex.photo.loaders.PhotoBitmapLoader.BitmapResult; +import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -92,31 +92,24 @@ public class ImageUtils { * Create a bitmap from a local URI * * @param resolver The ContentResolver - * @param uri The local URI - * @param maxSize The maximum size (either width or height) - * + * @param uri The local URI + * @param maxSize The maximum size (either width or height) * @return The new bitmap or null */ - public static BitmapResult createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) { - // TODO: make this method not download the image for both getImageBounds and decodeStream - BitmapResult result = new BitmapResult(); - InputStream inputStream = null; + public static BitmapResult createLocalBitmap(final ContentResolver resolver, final Uri uri, + final int maxSize) { + final BitmapResult result = new BitmapResult(); + final InputStreamFactory factory = createInputStreamFactory(resolver, uri); try { - final BitmapFactory.Options opts = new BitmapFactory.Options(); - final Point bounds = getImageBounds(resolver, uri); - inputStream = openInputStream(resolver, uri); - if (bounds == null || inputStream == null) { + final Point bounds = getImageBounds(factory); + if (bounds == null) { result.status = BitmapResult.STATUS_EXCEPTION; return result; } - opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize); - final Bitmap decodedBitmap = decodeStream(inputStream, null, opts); - - // Correct thumbnail orientation as necessary - // TODO: Fix rotation if it's actually a problem - //return rotateBitmap(resolver, uri, decodedBitmap); - result.bitmap = decodedBitmap; + final BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize); + result.bitmap = decodeStream(factory, null, opts); result.status = BitmapResult.STATUS_SUCCESS; return result; @@ -128,13 +121,6 @@ public class ImageUtils { // Do nothing - the photo will appear to be missing } catch (SecurityException exception) { result.status = BitmapResult.STATUS_EXCEPTION; - } finally { - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException ignore) { - } } return result; } @@ -144,46 +130,39 @@ public class ImageUtils { * BitmapFactory.Options)} that returns {@code null} on {@link * OutOfMemoryError}. * - * @param is The input stream that holds the raw data to be decoded into a - * bitmap. + * @param factory Used to create input streams that holds the raw data to be decoded into a + * bitmap. * @param outPadding If not null, return the padding rect for the bitmap if * it exists, otherwise set padding to [-1,-1,-1,-1]. If * no bitmap is returned (null) then padding is * unchanged. - * @param opts null-ok; Options that control downsampling and whether the - * image should be completely decoded, or just is size returned. + * @param opts null-ok; Options that control downsampling and whether the + * image should be completely decoded, or just is size returned. * @return The decoded bitmap, or null if the image data could not be - * decoded, or, if opts is non-null, if opts requested only the - * size be returned (in opts.outWidth and opts.outHeight) + * decoded, or, if opts is non-null, if opts requested only the + * size be returned (in opts.outWidth and opts.outHeight) */ - public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) { - ByteArrayOutputStream out = null; - InputStream byteStream = null; + public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding, + final BitmapFactory.Options opts) throws FileNotFoundException { + InputStream is = null; try { - out = new ByteArrayOutputStream(); - final byte[] buffer = new byte[4096]; - int n = is.read(buffer); - while (n >= 0) { - out.write(buffer, 0, n); - n = is.read(buffer); - } - - final byte[] bitmapBytes = out.toByteArray(); - // Determine the orientation for this image - final int orientation = Exif.getOrientation(bitmapBytes); - - // Create an InputStream from this byte array - byteStream = new ByteArrayInputStream(bitmapBytes); + is = factory.createInputStream(); + final int orientation = Exif.getOrientation(is, -1); + is.close(); - final Bitmap originalBitmap = BitmapFactory.decodeStream(byteStream, outPadding, opts); + // Decode the bitmap + is = factory.createInputStream(); + final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts); - if (byteStream != null && originalBitmap == null && !opts.inJustDecodeBounds) { + if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) { Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): " + "Image bytes cannot be decoded into a Bitmap"); throw new UnsupportedOperationException( "Image bytes cannot be decoded into a Bitmap."); } + + // Rotate the Bitmap based on the orientation if (originalBitmap != null && orientation != 0) { final Matrix matrix = new Matrix(); matrix.postRotate(orientation); @@ -198,16 +177,9 @@ public class ImageUtils { Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe); return null; } finally { - if (out != null) { + if (is != null) { try { - out.close(); - } catch (IOException e) { - // Do nothing - } - } - if (byteStream != null) { - try { - byteStream.close(); + is.close(); } catch (IOException e) { // Do nothing } @@ -218,73 +190,148 @@ public class ImageUtils { /** * Gets the image bounds * - * @param resolver The ContentResolver - * @param uri The uri + * @param factory Used to create the InputStream. * * @return The image bounds */ - private static Point getImageBounds(ContentResolver resolver, Uri uri) + private static Point getImageBounds(final InputStreamFactory factory) throws IOException { final BitmapFactory.Options opts = new BitmapFactory.Options(); - InputStream inputStream = null; - String scheme = uri.getScheme(); - try { - opts.inJustDecodeBounds = true; - inputStream = openInputStream(resolver, uri); - if (inputStream == null) { - return null; - } - decodeStream(inputStream, null, opts); + opts.inJustDecodeBounds = true; + decodeStream(factory, null, opts); - return new Point(opts.outWidth, opts.outHeight); - } finally { - try { - if (inputStream != null) { - inputStream.close(); + return new Point(opts.outWidth, opts.outHeight); + } + + private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver, + final Uri uri) { + final String scheme = uri.getScheme(); + if ("http".equals(scheme) || "https".equals(scheme)) { + return new HttpInputStreamFactory(resolver, uri); + } else if ("data".equals(scheme)) { + return new DataInputStreamFactory(resolver, uri); + } + return new BaseInputStreamFactory(resolver, uri); + } + + /** + * Utility class for when an InputStream needs to be read multiple times. For example, one pass + * may load EXIF orientation, and the second pass may do the actual Bitmap decode. + */ + public interface InputStreamFactory { + + /** + * Create a new InputStream. The caller of this method must be able to read the input + * stream starting from the beginning. + * @return + */ + InputStream createInputStream() throws FileNotFoundException; + } + + private static class BaseInputStreamFactory implements InputStreamFactory { + protected final ContentResolver mResolver; + protected final Uri mUri; + + public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) { + mResolver = resolver; + mUri = uri; + } + + @Override + public InputStream createInputStream() throws FileNotFoundException { + return mResolver.openInputStream(mUri); + } + } + + private static class DataInputStreamFactory extends BaseInputStreamFactory { + private byte[] mData; + + public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) { + super(resolver, uri); + } + + @Override + public InputStream createInputStream() throws FileNotFoundException { + if (mData == null) { + mData = parseDataUri(mUri); + if (mData == null) { + return super.createInputStream(); } - } catch (IOException ignore) { } + return new ByteArrayInputStream(mData); } - } - private static InputStream openInputStream(ContentResolver resolver, Uri uri) throws - FileNotFoundException { - String scheme = uri.getScheme(); - if ("http".equals(scheme) || "https".equals(scheme)) { + private byte[] parseDataUri(final Uri uri) { + final String ssp = uri.getSchemeSpecificPart(); try { - return new URL(uri.toString()).openStream(); - } catch (MalformedURLException e) { - // Fall-back to the previous behaviour, just in case - Log.w(TAG, "Could not convert the uri to url: " + uri.toString()); - return resolver.openInputStream(uri); - } catch (IOException e) { - Log.w(TAG, "Could not open input stream for uri: " + uri.toString()); + if (ssp.startsWith(BASE64_URI_PREFIX)) { + final String base64 = ssp.substring(BASE64_URI_PREFIX.length()); + return Base64.decode(base64, Base64.URL_SAFE); + } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){ + final String base64 = ssp.substring( + ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length()); + return Base64.decode(base64, Base64.DEFAULT); + } else { + return null; + } + } catch (IllegalArgumentException ex) { + Log.e(TAG, "Mailformed data URI: " + ex); return null; } - } else if ("data".equals(scheme)) { - byte[] data = parseDataUri(uri); - if (data != null) { - return new ByteArrayInputStream(data); - } } - return resolver.openInputStream(uri); } - private static byte[] parseDataUri(Uri uri) { - String ssp = uri.getSchemeSpecificPart(); - try { - if (ssp.startsWith(BASE64_URI_PREFIX)) { - String base64 = ssp.substring(BASE64_URI_PREFIX.length()); - return Base64.decode(base64, Base64.URL_SAFE); - } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){ - String base64 = ssp.substring( - ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length()); - return Base64.decode(base64, Base64.DEFAULT); - } else { - return null; + private static class HttpInputStreamFactory extends BaseInputStreamFactory { + private byte[] mData; + + public HttpInputStreamFactory(final ContentResolver resolver, final Uri uri) { + super(resolver, uri); + } + + @Override + public InputStream createInputStream() throws FileNotFoundException { + if (mData == null) { + mData = downloadBytes(); + if (mData == null) { + return super.createInputStream(); + } + } + return new ByteArrayInputStream(mData); + } + + private byte[] downloadBytes() throws FileNotFoundException { + InputStream is = null; + ByteArrayOutputStream out = null; + try { + try { + is = new URL(mUri.toString()).openStream(); + } catch (MalformedURLException e) { + return null; + } + out = new ByteArrayOutputStream(); + final byte[] buffer = new byte[4096]; + int n = is.read(buffer); + while (n >= 0) { + out.write(buffer, 0, n); + n = is.read(buffer); + } + + return out.toByteArray(); + } catch (IOException ignored) { + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ignored) { + } + } + if (out != null) { + try { + out.close(); + } catch (IOException ignored) { + } + } } - } catch (IllegalArgumentException ex) { - Log.e(TAG, "Mailformed data URI: " + ex); return null; } } diff --git a/src/com/android/ex/photo/util/InputStreamBuffer.java b/src/com/android/ex/photo/util/InputStreamBuffer.java new file mode 100644 index 0000000..0f82359 --- /dev/null +++ b/src/com/android/ex/photo/util/InputStreamBuffer.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2013 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.util; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +/** + * Wrapper for {@link InputStream} that allows you to read bytes from it like a byte[]. An + * internal buffer is kept as small as possible to avoid large unnecessary allocations. + * + * <p/> + * Care must be taken so that the internal buffer is kept small. The best practice is to + * precalculate the maximum buffer size that you will need. For example, + * say you have a loop that reads bytes from index <code>0</code> to <code>10</code>, + * skips to index <code>N</code>, reads from index <code>N</code> to <code>N+10</code>, etc. Then + * you know that the internal buffer can have a maximum size of <code>10</code>, + * and you should set the <code>bufferSize</code> parameter to <code>10</code> in the constructor. + * + * <p/> + * Use {@link #advanceTo(int)} to declare that you will not need to access lesser indexes. This + * helps to keep the internal buffer small. In the above example, after reading bytes from index + * <code>0</code> to <code>10</code>, you should call <code>advanceTo(N)</code> so that internal + * buffer becomes filled with bytes from index <code>N</code> to <code>N+10</code>. + * + * <p/> + * If you know that you are reading bytes from a <strong>strictly</strong> increasing or equal + * index, then you should set the <code>autoAdvance</code> parameter to <code>true</code> in the + * constructor. For complicated access patterns, or when you prefer to control the internal + * buffer yourself, set <code>autoAdvance</code> to <code>false</code>. When + * <code>autoAdvance</code> is enabled, every time an index is beyond the buffer length, + * the buffer will be shifted forward such that the index requested becomes the first element in + * the buffer. + * + * <p/> + * All public methods with parameter <code>index</code> are absolute indexed. The index is from + * the beginning of the wrapped input stream. + */ +public class InputStreamBuffer { + + private static final boolean DEBUG = false; + private static final int DEBUG_MAX_BUFFER_SIZE = 80; + private static final String TAG = "InputStreamBuffer"; + + private InputStream mInputStream; + private byte[] mBuffer; + private boolean mAutoAdvance; + /** Byte count the buffer is offset by. */ + private int mOffset = 0; + /** Number of bytes filled in the buffer. */ + private int mFilled = 0; + + /** + * Construct a new wrapper for an InputStream. + * + * <p/> + * If <code>autoAdvance</code> is true, behavior is undefined if you call {@link #get(int)} + * or {@link #has(int)} with an index N, then some arbitrary time later call {@link #get(int)} + * or {@link #has(int)} with an index M < N. The wrapper may return the right value, + * if the buffer happens to still contain index M, but more likely it will throw an + * {@link IllegalStateException}. + * + * <p/> + * If <code>autoAdvance</code> is false, you must be diligent and call {@link #advanceTo(int)} + * at the appropriate times to ensure that the internal buffer is not unnecessarily resized + * and reallocated. + * + * @param inputStream The input stream to wrap. The input stream will not be closed by the + * wrapper. + * @param bufferSize The initial size for the internal buffer. The buffer size should be + * carefully chosen to avoid resizing and reallocating the internal buffer. + * The internal buffer size used will be the least power of two greater + * than this parameter. + * @param autoAdvance Determines the behavior when you need to read an index that is beyond + * the internal buffer size. If true, the internal buffer will shift so + * that the requested index becomes the first element. If false, + * the internal buffer size will grow to the smallest power of 2 which is + * greater than the requested index. + */ + public InputStreamBuffer(final InputStream inputStream, int bufferSize, + final boolean autoAdvance) { + mInputStream = inputStream; + if (bufferSize <= 0) { + throw new IllegalArgumentException( + String.format("Buffer size %d must be positive.", bufferSize)); + } + bufferSize = leastPowerOf2(bufferSize); + mBuffer = new byte[bufferSize]; + mAutoAdvance = autoAdvance; + } + + /** + * Attempt to get byte at the requested index from the wrapped input stream. If the internal + * buffer contains the requested index, return immediately. If the index is less than the + * head of the buffer, or the index is greater or equal to the size of the wrapped input stream, + * a runtime exception is thrown. + * + * <p/> + * If the index is not in the internal buffer, but it can be requested from the input stream, + * {@link #fill(int)} will be called first, and the byte at the index returned. + * + * <p/> + * You should always call {@link #has(int)} with the same index, unless you are sure that no + * exceptions will be thrown as described above. + * + * <p/> + * Consider calling {@link #advanceTo(int)} if you know that you will never request a lesser + * index in the future. + * @param index The requested index. + * @return The byte at that index. + */ + public byte get(final int index) throws IllegalStateException, IndexOutOfBoundsException { + Trace.beginSection("get"); + if (has(index)) { + final int i = index - mOffset; + Trace.endSection(); + return mBuffer[i]; + } else { + Trace.endSection(); + throw new IndexOutOfBoundsException( + String.format("Index %d beyond length.", index)); + } + } + + /** + * Attempt to return whether the requested index is within the size of the wrapped input + * stream. One side effect is {@link #fill(int)} will be called. + * + * <p/> + * If this method returns true, it is guaranteed that {@link #get(int)} with the same index + * will not fail. That means that if the requested index is within the size of the wrapped + * input stream, but the index is less than the head of the internal buffer, + * a runtime exception is thrown. + * + * <p/> + * See {@link #get(int)} for caveats. A lot of the same warnings about exceptions and + * <code>advanceTo()</code> apply. + * @param index The requested index. + * @return True if requested index is within the size of the wrapped input stream. False if + * the index is beyond the size. + */ + public boolean has(final int index) throws IllegalStateException, IndexOutOfBoundsException { + Trace.beginSection("has"); + if (index < mOffset) { + Trace.endSection(); + throw new IllegalStateException( + String.format("Index %d is before buffer %d", index, mOffset)); + } + + final int i = index - mOffset; + + // Requested index not in internal buffer. + if (i >= mFilled || i >= mBuffer.length) { + Trace.endSection(); + return fill(index); + } + + Trace.endSection(); + return true; + } + + /** + * Attempts to advance the head of the buffer to the requested index. If the index is less + * than the head of the buffer, the internal state will not be changed. + * + * <p/> + * Advancing does not fill the internal buffer. The next {@link #get(int)} or + * {@link #has(int)} call will fill the buffer. + */ + public void advanceTo(final int index) throws IllegalStateException, IndexOutOfBoundsException { + Trace.beginSection("advance to"); + final int i = index - mOffset; + if (i <= 0) { + // noop + Trace.endSection(); + return; + } else if (i < mFilled) { + // Shift elements starting at i to position 0. + shiftToBeginning(i); + mOffset = index; + mFilled = mFilled - i; + } else if (mInputStream != null) { + // Burn some bytes from the input stream to match the new index. + int burn = i - mFilled; + boolean empty = false; + int fails = 0; + try { + while (burn > 0) { + final long burned = mInputStream.skip(burn); + if (burned <= 0) { + fails++; + } else { + burn -= burned; + } + + if (fails >= 5) { + empty = true; + break; + } + } + } catch (IOException ignored) { + empty = true; + } + + if (empty) { + //Mark input stream as consumed. + mInputStream = null; + } + + mOffset = index - burn; + mFilled = 0; + } else { + // Advancing beyond the input stream. + mOffset = index; + mFilled = 0; + } + + Log.d(TAG, String.format("advanceTo %d buffer: %s", i, this)); + Trace.endSection(); + } + + /** + * Attempt to fill the internal buffer fully. The buffer will be modified such that the + * requested index will always be in the buffer. If the index is less + * than the head of the buffer, a runtime exception is thrown. + * + * <p/> + * If the requested index is already in bounds of the buffer, then the buffer will just be + * filled. + * + * <p/> + * Otherwise, if <code>autoAdvance</code> was set to true in the constructor, + * {@link #advanceTo(int)} will be called with the requested index, + * and then the buffer filled. If <code>autoAdvance</code> was set to false, + * we allocate a single larger buffer of a least multiple-of-two size that can contain the + * requested index. The elements in the old buffer are copied over to the head of the new + * buffer. Then the entire buffer is filled. + * @param index The requested index. + * @return True if the byte at the requested index has been filled. False if the wrapped + * input stream ends before we reach the index. + */ + private boolean fill(final int index) { + Trace.beginSection("fill"); + if (index < mOffset) { + Trace.endSection(); + throw new IllegalStateException( + String.format("Index %d is before buffer %d", index, mOffset)); + } + + int i = index - mOffset; + // Can't fill buffer anymore if input stream is consumed. + if (mInputStream == null) { + Trace.endSection(); + return false; + } + + // Increase buffer size if necessary. + int length = i + 1; + if (length > mBuffer.length) { + if (mAutoAdvance) { + advanceTo(index); + i = index - mOffset; + } else { + length = leastPowerOf2(length); + Log.w(TAG, String.format( + "Increasing buffer length from %d to %d. Bad buffer size chosen, " + + "or advanceTo() not called.", + mBuffer.length, length)); + mBuffer = Arrays.copyOf(mBuffer, length); + } + } + + // Read from input stream to fill buffer. + int read = -1; + try { + read = mInputStream.read(mBuffer, mFilled, mBuffer.length - mFilled); + } catch (IOException ignored) { + } + + if (read != -1) { + mFilled = mFilled + read; + } else { + // Mark input stream as consumed. + mInputStream = null; + } + + Log.d(TAG, String.format("fill %d buffer: %s", i, this)); + + Trace.endSection(); + return i < mFilled; + } + + /** + * Modify the internal buffer so that all the bytes are shifted towards the head by + * <code>i</code>. In other words, the byte at index <code>i</code> will now be at index + * <code>0</code>. Bytes from a lesser index are tossed. + * @param i How much to shift left. + */ + private void shiftToBeginning(final int i) { + if (i >= mBuffer.length) { + throw new IndexOutOfBoundsException( + String.format("Index %d out of bounds. Length %d", i, mBuffer.length)); + } + for (int j = 0; j + i < mFilled; j++) { + mBuffer[j] = mBuffer[j + i]; + } + } + + @Override + public String toString() { + if (DEBUG) { + return toDebugString(); + } + return String.format("+%d+%d [%d]", mOffset, mBuffer.length, mFilled); + } + + public String toDebugString() { + Trace.beginSection("to debug string"); + final StringBuilder sb = new StringBuilder(); + sb.append("+").append(mOffset); + sb.append("+").append(mBuffer.length); + sb.append(" ["); + for (int i = 0; i < mBuffer.length && i < DEBUG_MAX_BUFFER_SIZE; i++) { + if (i > 0) { + sb.append(","); + } + if (i < mFilled) { + sb.append(String.format("%02X", mBuffer[i])); + } else { + sb.append("__"); + } + } + if (mInputStream != null) { + sb.append("..."); + } + sb.append("]"); + + Trace.endSection(); + return sb.toString(); + } + + /** + * Calculate the least power of two greater than or equal to the input. + */ + private static int leastPowerOf2(int n) { + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + n++; + return n; + } +} diff --git a/src/com/android/ex/photo/util/Trace.java b/src/com/android/ex/photo/util/Trace.java new file mode 100644 index 0000000..9645ed4 --- /dev/null +++ b/src/com/android/ex/photo/util/Trace.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 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.util; + +import android.os.Build; + +/** + * Stand-in for {@link android.os.Trace}. + */ +public abstract class Trace { + + /** + * Begins systrace tracing for a given tag. No-op on unsupported platform versions. + * + * @param tag systrace tag to use + * + * @see android.os.Trace#beginSection(String) + */ + public static void beginSection(String tag) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + android.os.Trace.beginSection(tag); + } + } + + /** + * Ends systrace tracing for the most recently begun section. No-op on unsupported platform + * versions. + * + * @see android.os.Trace#endSection() + */ + public static void endSection() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + android.os.Trace.endSection(); + } + } + +} diff --git a/src/com/android/ex/photo/views/PhotoView.java b/src/com/android/ex/photo/views/PhotoView.java index 0bc704e..7ba096d 100644 --- a/src/com/android/ex/photo/views/PhotoView.java +++ b/src/com/android/ex/photo/views/PhotoView.java @@ -28,12 +28,14 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.support.v4.view.GestureDetectorCompat; +import android.support.v4.view.ScaleGestureDetectorCompat; import android.util.AttributeSet; import android.view.GestureDetector.OnGestureListener; import android.view.GestureDetector.OnDoubleTapListener; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; +import android.view.ViewConfiguration; import com.android.ex.photo.R; import com.android.ex.photo.fragments.PhotoViewFragment.HorizontallyScrollable; @@ -59,6 +61,12 @@ public class PhotoView extends View implements OnGestureListener, /** The width & height of the bitmap returned by {@link #getCroppedPhoto()} */ private final static float CROPPED_SIZE = 256.0f; + /** + * Touch slop used to determine if this double tap is valid for starting a scale or should be + * ignored. + */ + private static int sTouchSlopSquare; + /** If {@code true}, the static values have been initialized */ private static boolean sInitialized; @@ -154,6 +162,24 @@ public class PhotoView extends View implements OnGestureListener, /** Array to store a copy of the matrix values */ private float[] mValues = new float[9]; + /** + * Track whether a double tap event occurred. + */ + private boolean mDoubleTapOccurred; + + /** + * X and Y coordinates for the current down event. Since mDoubleTapOccurred only contains the + * information that there was a double tap event, use these to get the secondary tap + * information to determine if a user has moved beyond touch slop. + */ + private float mDownFocusX; + private float mDownFocusY; + + /** + * Whether the QuickSale gesture is enabled. + */ + private boolean mQuickScaleEnabled; + public PhotoView(Context context) { super(context); initialize(); @@ -194,7 +220,48 @@ public class PhotoView extends View implements OnGestureListener, @Override public boolean onDoubleTap(MotionEvent e) { - if (mDoubleTapToZoomEnabled && mTransformsEnabled) { + mDoubleTapOccurred = true; + if (!mQuickScaleEnabled) { + return scale(e); + } + return false; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + final int action = e.getAction(); + boolean handled = false; + + switch (action) { + case MotionEvent.ACTION_DOWN: + if (mQuickScaleEnabled) { + mDownFocusX = e.getX(); + mDownFocusY = e.getY(); + } + break; + case MotionEvent.ACTION_UP: + if (mQuickScaleEnabled) { + handled = scale(e); + } + break; + case MotionEvent.ACTION_MOVE: + if (mQuickScaleEnabled && mDoubleTapOccurred) { + final int deltaX = (int) (e.getX() - mDownFocusX); + final int deltaY = (int) (e.getY() - mDownFocusY); + int distance = (deltaX * deltaX) + (deltaY * deltaY); + if (distance > sTouchSlopSquare) { + mDoubleTapOccurred = false; + } + } + break; + + } + return handled; + } + + private boolean scale(MotionEvent e) { + boolean handled = false; + if (mDoubleTapToZoomEnabled && mTransformsEnabled && mDoubleTapOccurred) { if (!mDoubleTapDebounce) { float currentScale = getScale(); float targetScale = currentScale * DOUBLE_TAP_SCALE_FACTOR; @@ -204,15 +271,12 @@ public class PhotoView extends View implements OnGestureListener, targetScale = Math.min(mMaxScale, targetScale); mScaleRunnable.start(currentScale, targetScale, e.getX(), e.getY()); + handled = true; } mDoubleTapDebounce = false; } - return true; - } - - @Override - public boolean onDoubleTapEvent(MotionEvent e) { - return true; + mDoubleTapOccurred = false; + return handled; } @Override @@ -379,6 +443,7 @@ public class PhotoView extends View implements OnGestureListener, mRotateRunnable = null; setOnClickListener(null); mExternalClickListener = null; + mDoubleTapOccurred = false; } /** @@ -933,10 +998,15 @@ public class PhotoView extends View implements OnGestureListener, sCropPaint.setColor(resources.getColor(R.color.photo_crop_highlight_color)); sCropPaint.setStyle(Style.STROKE); sCropPaint.setStrokeWidth(resources.getDimension(R.dimen.photo_crop_stroke_width)); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + final int touchSlop = configuration.getScaledTouchSlop(); + sTouchSlopSquare = touchSlop * touchSlop; } mGestureDetector = new GestureDetectorCompat(context, this, null); mScaleGetureDetector = new ScaleGestureDetector(context, this); + mQuickScaleEnabled = ScaleGestureDetectorCompat.isQuickScaleEnabled(mScaleGetureDetector); mScaleRunnable = new ScaleRunnable(this); mTranslateRunnable = new TranslateRunnable(this); mSnapRunnable = new SnapRunnable(this); |
