summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRicardo Cerqueira <cyanogenmod@cerqueira.org>2013-11-01 16:05:26 +0000
committerRicardo Cerqueira <cyanogenmod@cerqueira.org>2013-11-01 16:05:26 +0000
commiteb20264840b2045b25e6bd0bb801ac8905ed0498 (patch)
tree62e3d6a609af720cd46fdb520afbf01905ccad76
parentd98118500eb48b057f5f296cea280a84ac4ede94 (diff)
parentd54854a4364b03dd1eb5d4787a72da2b430a16a7 (diff)
downloadandroid_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
Android 4.4 Release 1.0
-rw-r--r--Android.mk32
-rw-r--r--AndroidManifest.xml4
-rw-r--r--activity/res/values/themes.xml (renamed from res/values/themes.xml)0
-rw-r--r--activity/src/com/android/ex/photo/PhotoViewActivity.java1072
-rw-r--r--appcompat/res/values/themes.xml32
-rw-r--r--appcompat/src/com/android/ex/photo/PhotoViewActivity.java1071
-rw-r--r--res/drawable-hdpi/actionbar_translucent.9.pngbin155 -> 152 bytes
-rw-r--r--res/drawable-mdpi/actionbar_translucent.9.pngbin153 -> 140 bytes
-rw-r--r--res/drawable-xhdpi/actionbar_translucent.9.pngbin0 -> 161 bytes
-rw-r--r--res/drawable-xxhdpi/actionbar_translucent.9.pngbin0 -> 250 bytes
-rw-r--r--res/drawable-xxhdpi/ic_menu_refresh_holo_dark.pngbin0 -> 2476 bytes
-rw-r--r--res/layout-v11/photo_retry_button.xml12
-rw-r--r--res/layout/photo_activity_view.xml13
-rw-r--r--res/layout/photo_fragment_view.xml13
-rw-r--r--res/layout/photo_retry_button.xml11
-rw-r--r--res/values-en-rIN/strings.xml24
-rw-r--r--res/values-et-rEE/strings.xml24
-rw-r--r--res/values-fr-rCA/strings.xml24
-rw-r--r--res/values-hy-rAM/strings.xml24
-rw-r--r--res/values-ka-rGE/strings.xml24
-rw-r--r--res/values-km-rKH/strings.xml24
-rw-r--r--res/values-lo-rLA/strings.xml24
-rw-r--r--res/values-mn-rMN/strings.xml24
-rw-r--r--res/values-ms-rMY/strings.xml24
-rw-r--r--res/values-zh-rHK/strings.xml24
-rw-r--r--sample/Android.mk12
-rw-r--r--sample/AndroidManifest.xml3
-rw-r--r--sample/res/menu/activity_main.xml6
-rw-r--r--sample/res/values-en-rIN/strings.xml8
-rw-r--r--sample/res/values-et-rEE/strings.xml8
-rw-r--r--sample/res/values-fr-rCA/strings.xml8
-rw-r--r--sample/res/values-hy-rAM/strings.xml8
-rw-r--r--sample/res/values-ka-rGE/strings.xml8
-rw-r--r--sample/res/values-km-rKH/strings.xml8
-rw-r--r--sample/res/values-lo-rLA/strings.xml8
-rw-r--r--sample/res/values-mn-rMN/strings.xml8
-rw-r--r--sample/res/values-ms-rMY/strings.xml8
-rw-r--r--sample/res/values-zh-rHK/strings.xml8
-rw-r--r--src/com/android/ex/photo/Intents.java95
-rw-r--r--src/com/android/ex/photo/PhotoViewActivity.java620
-rw-r--r--src/com/android/ex/photo/PhotoViewCallbacks.java15
-rw-r--r--src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java6
-rw-r--r--src/com/android/ex/photo/adapters/PhotoPagerAdapter.java7
-rw-r--r--src/com/android/ex/photo/fragments/PhotoViewFragment.java180
-rw-r--r--src/com/android/ex/photo/loaders/PhotoBitmapLoader.java15
-rw-r--r--src/com/android/ex/photo/loaders/PhotoBitmapLoaderInterface.java18
-rw-r--r--src/com/android/ex/photo/provider/PhotoContract.java5
-rw-r--r--src/com/android/ex/photo/util/Exif.java100
-rw-r--r--src/com/android/ex/photo/util/ImageUtils.java267
-rw-r--r--src/com/android/ex/photo/util/InputStreamBuffer.java372
-rw-r--r--src/com/android/ex/photo/util/Trace.java51
-rw-r--r--src/com/android/ex/photo/views/PhotoView.java84
52 files changed, 3575 insertions, 861 deletions
diff --git a/Android.mk b/Android.mk
index ba08283..7a6d2f1 100644
--- a/Android.mk
+++ b/Android.mk
@@ -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
index f18761f..4b40967 100644
--- a/res/drawable-hdpi/actionbar_translucent.9.png
+++ b/res/drawable-hdpi/actionbar_translucent.9.png
Binary files differ
diff --git a/res/drawable-mdpi/actionbar_translucent.9.png b/res/drawable-mdpi/actionbar_translucent.9.png
index f78fb8a..a995d44 100644
--- a/res/drawable-mdpi/actionbar_translucent.9.png
+++ b/res/drawable-mdpi/actionbar_translucent.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/actionbar_translucent.9.png b/res/drawable-xhdpi/actionbar_translucent.9.png
new file mode 100644
index 0000000..f4ed5fa
--- /dev/null
+++ b/res/drawable-xhdpi/actionbar_translucent.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/actionbar_translucent.9.png b/res/drawable-xxhdpi/actionbar_translucent.9.png
new file mode 100644
index 0000000..13e4dbc
--- /dev/null
+++ b/res/drawable-xxhdpi/actionbar_translucent.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_menu_refresh_holo_dark.png b/res/drawable-xxhdpi/ic_menu_refresh_holo_dark.png
new file mode 100644
index 0000000..088d76b
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_menu_refresh_holo_dark.png
Binary files differ
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);