summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--carousel/test/res/values-in/strings.xml2
-rw-r--r--chips/res/values-ko/strings.xml2
-rw-r--r--common/java/com/android/common/OperationScheduler.java53
-rw-r--r--common/tests/src/com/android/common/OperationSchedulerTest.java40
-rw-r--r--photoviewer/.gitignore8
-rw-r--r--photoviewer/AndroidManifest.xml3
-rw-r--r--photoviewer/src/com/android/ex/photo/Intents.java33
-rw-r--r--photoviewer/src/com/android/ex/photo/PhotoViewActivity.java59
-rw-r--r--photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java22
-rw-r--r--photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java4
-rw-r--r--photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java23
-rw-r--r--photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java41
-rw-r--r--photoviewer/src/com/android/ex/photo/provider/PhotoContract.java5
-rw-r--r--photoviewer/src/com/android/ex/photo/util/ImageUtils.java52
-rw-r--r--photoviewer/src/com/android/ex/photo/views/PhotoView.java16
15 files changed, 298 insertions, 65 deletions
diff --git a/carousel/test/res/values-in/strings.xml b/carousel/test/res/values-in/strings.xml
index 40000e8..06c8812 100644
--- a/carousel/test/res/values-in/strings.xml
+++ b/carousel/test/res/values-in/strings.xml
@@ -21,7 +21,7 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="music_demo_activity_label" msgid="4382090808250495841">"KaruselMusik"</string>
<string name="carousel_test_activity_label" msgid="6014624482213318747">"UjiKarusel"</string>
- <string name="carousel_test_activity_description" msgid="1632693812604375483">"Aplikasi untuk menampilkan penggunaan Karusel"</string>
+ <string name="carousel_test_activity_description" msgid="1632693812604375483">"Aplikasi untuk menampilkan penggunaan Korsel"</string>
<string name="task_switcher_activity_label" msgid="714620143340933546">"PengubahTugas"</string>
<string name="recent_tasks_title" msgid="1030287226205477117">"Aplikasi Terbaru"</string>
<string name="no_recent_tasks" msgid="6884096266670555780">"Tidak ada tugas terbaru"</string>
diff --git a/chips/res/values-ko/strings.xml b/chips/res/values-ko/strings.xml
index 7423ce5..f7884bd 100644
--- a/chips/res/values-ko/strings.xml
+++ b/chips/res/values-ko/strings.xml
@@ -19,5 +19,5 @@
<string name="more_string" msgid="8495478259330621990">"<xliff:g id="COUNT">%1$s</xliff:g>명 이상"</string>
<string name="copy_email" msgid="7869435992461603532">"이메일 주소 복사"</string>
<string name="copy_number" msgid="530057841276106843">"전화번호 복사"</string>
- <string name="done" msgid="2356320650733788862">"Enter 키"</string>
+ <string name="done" msgid="2356320650733788862">"입력"</string>
</resources>
diff --git a/common/java/com/android/common/OperationScheduler.java b/common/java/com/android/common/OperationScheduler.java
index b8fc7bc..261b15d 100644
--- a/common/java/com/android/common/OperationScheduler.java
+++ b/common/java/com/android/common/OperationScheduler.java
@@ -42,6 +42,9 @@ public class OperationScheduler {
/** Wait this long times the number of consecutive errors so far before retrying. */
public long backoffIncrementalMillis = 5000;
+ /** Wait this long times 2^(number of consecutive errors so far) before retrying. */
+ public int backoffExponentialMillis = 0;
+
/** Maximum duration of moratorium to honor. Mostly an issue for clock rollbacks. */
public long maxMoratoriumMillis = 24 * 3600 * 1000;
@@ -53,11 +56,20 @@ public class OperationScheduler {
@Override
public String toString() {
- return String.format(
+ if (backoffExponentialMillis > 0) {
+ return String.format(
+ "OperationScheduler.Options[backoff=%.1f+%.1f+%.1f max=%.1f min=%.1f period=%.1f]",
+ backoffFixedMillis / 1000.0, backoffIncrementalMillis / 1000.0,
+ backoffExponentialMillis / 1000.0,
+ maxMoratoriumMillis / 1000.0, minTriggerMillis / 1000.0,
+ periodicIntervalMillis / 1000.0);
+ } else {
+ return String.format(
"OperationScheduler.Options[backoff=%.1f+%.1f max=%.1f min=%.1f period=%.1f]",
backoffFixedMillis / 1000.0, backoffIncrementalMillis / 1000.0,
maxMoratoriumMillis / 1000.0, minTriggerMillis / 1000.0,
periodicIntervalMillis / 1000.0);
+ }
}
}
@@ -76,7 +88,7 @@ public class OperationScheduler {
* Parse scheduler options supplied in this string form:
*
* <pre>
- * backoff=(fixed)+(incremental) max=(maxmoratorium) min=(mintrigger) [period=](interval)
+ * backoff=(fixed)+(incremental)[+(exponential)] max=(maxmoratorium) min=(mintrigger) [period=](interval)
* </pre>
*
* All values are times in (possibly fractional) <em>seconds</em> (not milliseconds).
@@ -97,14 +109,18 @@ public class OperationScheduler {
for (String param : spec.split(" +")) {
if (param.length() == 0) continue;
if (param.startsWith("backoff=")) {
- int plus = param.indexOf('+', 8);
- if (plus < 0) {
- options.backoffFixedMillis = parseSeconds(param.substring(8));
- } else {
- if (plus > 8) {
- options.backoffFixedMillis = parseSeconds(param.substring(8, plus));
- }
- options.backoffIncrementalMillis = parseSeconds(param.substring(plus + 1));
+ String[] pieces = param.substring(8).split("\\+");
+ if (pieces.length > 3) {
+ throw new IllegalArgumentException("bad value for backoff: [" + spec + "]");
+ }
+ if (pieces.length > 0 && pieces[0].length() > 0) {
+ options.backoffFixedMillis = parseSeconds(pieces[0]);
+ }
+ if (pieces.length > 1 && pieces[1].length() > 0) {
+ options.backoffIncrementalMillis = parseSeconds(pieces[1]);
+ }
+ if (pieces.length > 2 && pieces[2].length() > 0) {
+ options.backoffExponentialMillis = (int)parseSeconds(pieces[2]);
}
} else if (param.startsWith("max=")) {
options.maxMoratoriumMillis = parseSeconds(param.substring(4));
@@ -160,8 +176,21 @@ public class OperationScheduler {
time = Math.max(time, moratoriumTimeMillis);
time = Math.max(time, lastSuccessTimeMillis + options.minTriggerMillis);
if (errorCount > 0) {
- time = Math.max(time, lastErrorTimeMillis + options.backoffFixedMillis +
- options.backoffIncrementalMillis * errorCount);
+ int shift = errorCount-1;
+ // backoffExponentialMillis is an int, so we can safely
+ // double it 30 times without overflowing a long.
+ if (shift > 30) shift = 30;
+ long backoff = options.backoffFixedMillis +
+ (options.backoffIncrementalMillis * errorCount) +
+ (((long)options.backoffExponentialMillis) << shift);
+
+ // Treat backoff like a moratorium: don't let the backoff
+ // time grow too large.
+ if (moratoriumTimeMillis > 0 && backoff > moratoriumTimeMillis) {
+ backoff = moratoriumTimeMillis;
+ }
+
+ time = Math.max(time, lastErrorTimeMillis + backoff);
}
return time;
}
diff --git a/common/tests/src/com/android/common/OperationSchedulerTest.java b/common/tests/src/com/android/common/OperationSchedulerTest.java
index 955508f..87e2cd8 100644
--- a/common/tests/src/com/android/common/OperationSchedulerTest.java
+++ b/common/tests/src/com/android/common/OperationSchedulerTest.java
@@ -119,6 +119,42 @@ public class OperationSchedulerTest extends AndroidTestCase {
assertEquals(beforeSuccess + 1000000, scheduler.getNextTimeMillis(options));
}
+ @MediumTest
+ public void testExponentialBackoff() throws Exception {
+ TimeTravelScheduler scheduler = new TimeTravelScheduler();
+ OperationScheduler.Options options = new OperationScheduler.Options();
+ options.backoffFixedMillis = 100;
+ options.backoffIncrementalMillis = 1000;
+ options.backoffExponentialMillis = 10000;
+ scheduler.setTriggerTimeMillis(0);
+ scheduler.setEnabledState(true);
+
+ // Backoff interval after an error
+ long beforeError = (scheduler.timeMillis += 10);
+ scheduler.onTransientError();
+ assertEquals(0, scheduler.getLastSuccessTimeMillis());
+ assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
+ assertEquals(beforeError + 11100, scheduler.getNextTimeMillis(options));
+
+ // Second error
+ beforeError = (scheduler.timeMillis += 10);
+ scheduler.onTransientError();
+ assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
+ assertEquals(beforeError + 22100, scheduler.getNextTimeMillis(options));
+
+ // Third error
+ beforeError = (scheduler.timeMillis += 10);
+ scheduler.onTransientError();
+ assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
+ assertEquals(beforeError + 43100, scheduler.getNextTimeMillis(options));
+
+ // Fourth error
+ beforeError = (scheduler.timeMillis += 10);
+ scheduler.onTransientError();
+ assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
+ assertEquals(beforeError + 84100, scheduler.getNextTimeMillis(options));
+ }
+
@SmallTest
public void testParseOptions() throws Exception {
OperationScheduler.Options options = new OperationScheduler.Options();
@@ -138,6 +174,10 @@ public class OperationSchedulerTest extends AndroidTestCase {
assertEquals(
"OperationScheduler.Options[backoff=10.0+2.5 max=12345.6 min=7.0 period=3800.0]",
OperationScheduler.parseOptions("", options).toString());
+
+ assertEquals(
+ "OperationScheduler.Options[backoff=5.0+2.5+10.0 max=12345.6 min=7.0 period=3600.0]",
+ OperationScheduler.parseOptions("backoff=5.0++10.0 3600", options).toString());
}
@SmallTest
diff --git a/photoviewer/.gitignore b/photoviewer/.gitignore
new file mode 100644
index 0000000..ff7ef7d
--- /dev/null
+++ b/photoviewer/.gitignore
@@ -0,0 +1,8 @@
+*~
+*.bak
+*.class
+bin/
+gen/
+*.properties
+.classpath
+.project
diff --git a/photoviewer/AndroidManifest.xml b/photoviewer/AndroidManifest.xml
index 485e044..e4e9101 100644
--- a/photoviewer/AndroidManifest.xml
+++ b/photoviewer/AndroidManifest.xml
@@ -18,4 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ex.photo"
android:versionCode="1">
+
+ <uses-sdk
+ android:minSdkVersion="11"/>
</manifest> \ No newline at end of file
diff --git a/photoviewer/src/com/android/ex/photo/Intents.java b/photoviewer/src/com/android/ex/photo/Intents.java
index 0e64730..24c48fe 100644
--- a/photoviewer/src/com/android/ex/photo/Intents.java
+++ b/photoviewer/src/com/android/ex/photo/Intents.java
@@ -29,11 +29,12 @@ import com.android.ex.photo.fragments.PhotoViewFragment;
public class Intents {
// Intent extras
public static final String EXTRA_PHOTO_INDEX = "photo_index";
- public static final String EXTRA_PHOTO_ID = "photo_id";
+ public static final String EXTRA_INITIAL_PHOTO_URI = "initial_photo_uri";
public static final String EXTRA_PHOTOS_URI = "photos_uri";
public static final String EXTRA_RESOLVED_PHOTO_URI = "resolved_photo_uri";
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";
/**
* Gets a photo view intent builder to display the photos from phone activity.
@@ -67,6 +68,8 @@ public class Intents {
/** The index of the photo to show */
private Integer mPhotoIndex;
+ /** The URI of the initial photo to show */
+ private String mInitialPhotoUri;
/** The URI of the group of photos to display */
private String mPhotosUri;
/** The URL of the photo to display */
@@ -75,6 +78,8 @@ public class Intents {
private String[] mProjection;
/** The URI of a thumbnail of the photo to display */
private String mThumbnailUri;
+ /** The maximum scale to display images at before */
+ private Float mMaxInitialScale;
private PhotoViewIntentBuilder(Context context, Class<?> cls) {
mIntent = new Intent(context, cls);
@@ -86,6 +91,12 @@ public class Intents {
return this;
}
+ /** Sets the initial photo URI */
+ public PhotoViewIntentBuilder setInitialPhotoUri(String initialPhotoUri) {
+ mInitialPhotoUri = initialPhotoUri;
+ return this;
+ }
+
/** Sets the photos URI */
public PhotoViewIntentBuilder setPhotosUri(String photosUri) {
mPhotosUri = photosUri;
@@ -116,6 +127,14 @@ public class Intents {
return this;
}
+ /**
+ * Sets the maximum scale which an image is initially displayed at
+ */
+ public PhotoViewIntentBuilder setMaxInitialScale(float maxScale) {
+ mMaxInitialScale = maxScale;
+ return this;
+ }
+
/** Build the intent */
public Intent build() {
mIntent.setAction(Intent.ACTION_VIEW);
@@ -126,6 +145,14 @@ public class Intents {
mIntent.putExtra(EXTRA_PHOTO_INDEX, (int) mPhotoIndex);
}
+ if (mInitialPhotoUri != null) {
+ mIntent.putExtra(EXTRA_INITIAL_PHOTO_URI, mInitialPhotoUri);
+ }
+ if (mInitialPhotoUri != null && mPhotoIndex != null) {
+ throw new IllegalStateException(
+ "specified both photo index and photo uri");
+ }
+
if (mPhotosUri != null) {
mIntent.putExtra(EXTRA_PHOTOS_URI, mPhotosUri);
}
@@ -142,6 +169,10 @@ public class Intents {
mIntent.putExtra(EXTRA_THUMBNAIL_URI, mThumbnailUri);
}
+ if (mMaxInitialScale != null) {
+ mIntent.putExtra(EXTRA_MAX_INITIAL_SCALE, mMaxInitialScale);
+ }
+
return mIntent;
}
}
diff --git a/photoviewer/src/com/android/ex/photo/PhotoViewActivity.java b/photoviewer/src/com/android/ex/photo/PhotoViewActivity.java
index 2a24e1f..6c32ae5 100644
--- a/photoviewer/src/com/android/ex/photo/PhotoViewActivity.java
+++ b/photoviewer/src/com/android/ex/photo/PhotoViewActivity.java
@@ -31,6 +31,8 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
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;
@@ -111,6 +113,8 @@ public class PhotoViewActivity extends Activity implements
/** The URI of the photos we're viewing; may be {@code null} */
private String mPhotosUri;
+ /** The URI of the photo currently viewed photo */
+ private String mInitialPhotoUri;
/** The index of the currently viewed photo */
private int mPhotoIndex;
/** The query projection to use; may be {@code null} */
@@ -133,6 +137,8 @@ public class PhotoViewActivity extends Activity implements
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;
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..."
@@ -176,12 +182,19 @@ public class PhotoViewActivity extends Activity implements
if (mIntent.hasExtra(Intents.EXTRA_PHOTO_INDEX) && currentItem < 0) {
currentItem = mIntent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1);
}
+ if (mIntent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI) && currentItem < 0) {
+ 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);
+
mPhotoIndex = currentItem;
setContentView(R.layout.photo_activity_view);
// Create the adapter and add the view pager
- mAdapter = new PhotoPagerAdapter(this, getFragmentManager(), null);
+ mAdapter = new PhotoPagerAdapter(this, getFragmentManager(), null, mMaxInitialScale);
mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager);
mViewPager.setAdapter(mAdapter);
@@ -236,6 +249,16 @@ public class PhotoViewActivity extends Activity implements
outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen);
}
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
public void addScreenListener(OnScreenListener listener) {
mScreenListeners.add(listener);
}
@@ -296,6 +319,20 @@ public class PhotoViewActivity extends Activity implements
} 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
@@ -306,6 +343,9 @@ public class PhotoViewActivity extends Activity implements
}
mIsEmpty = false;
+ mAdapter.swapCursor(data);
+ notifyCursorListeners(data);
+
// set the selected photo
int itemIndex = mPhotoIndex;
@@ -314,9 +354,6 @@ public class PhotoViewActivity extends Activity implements
itemIndex = 0;
}
- mAdapter.swapCursor(data);
- notifyCursorListeners(data);
-
mViewPager.setCurrentItem(itemIndex, false);
setViewActivated();
}
@@ -398,7 +435,7 @@ public class PhotoViewActivity extends Activity implements
/**
* Updates the title bar according to the value of {@link #mFullScreen}.
*/
- private void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) {
+ protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) {
final boolean fullScreenChanged = (fullScreen != mFullScreen);
mFullScreen = fullScreen;
@@ -428,7 +465,7 @@ public class PhotoViewActivity extends Activity implements
mHandler.removeCallbacks(mActionBarHideRunnable);
}
- private void setLightsOutMode(boolean enabled) {
+ protected void setLightsOutMode(boolean enabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
int flags = enabled
? View.SYSTEM_UI_FLAG_LOW_PROFILE
@@ -457,7 +494,7 @@ public class PhotoViewActivity extends Activity implements
private Runnable mActionBarHideRunnable = new Runnable() {
@Override
public void run() {
- PhotoViewActivity.this.setLightsOutMode(true);
+ setFullScreen(true, true);
}
};
@@ -533,6 +570,14 @@ public class PhotoViewActivity extends Activity implements
}
}
+ protected boolean isFullScreen() {
+ return mFullScreen;
+ }
+
+ protected void setPhotoIndex(int index) {
+ mPhotoIndex = index;
+ }
+
public void onNewPhotoLoaded() {
}
}
diff --git a/photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java b/photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java
index 848d79a..ae2c92e 100644
--- a/photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java
+++ b/photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java
@@ -38,13 +38,12 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
private static final String TAG = "BaseCursorPagerAdapter";
Context mContext;
- private boolean mDataValid;
private Cursor mCursor;
private int mRowIDColumn;
/** Mapping of row ID to cursor position */
private SparseIntArray mItemPosition;
/** Mapping of instantiated object to row ID */
- private HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
+ private final HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
/**
* Constructor that always enables auto-requery.
@@ -67,9 +66,11 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
*/
public abstract Fragment getItem(Context context, Cursor cursor, int position);
+ // TODO: This shouldn't just return null - maybe it needs to wait for a cursor to be supplied?
+ // See b/7103023
@Override
public Fragment getItem(int position) {
- if (mDataValid && moveCursorTo(position)) {
+ if (mCursor != null && moveCursorTo(position)) {
return getItem(mContext, mCursor, position);
}
return null;
@@ -77,7 +78,7 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
@Override
public int getCount() {
- if (mDataValid && mCursor != null) {
+ if (mCursor != null) {
return mCursor.getCount();
} else {
return 0;
@@ -86,7 +87,7 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
@Override
public Object instantiateItem(View container, int position) {
- if (!mDataValid) {
+ if (mCursor == null) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
@@ -127,7 +128,7 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
* @return true if data is valid
*/
public boolean isDataValid() {
- return mDataValid;
+ return mCursor != null;
}
/**
@@ -141,7 +142,7 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
* Returns the data item associated with the specified position in the data set.
*/
public Object getDataItem(int position) {
- if (mDataValid && moveCursorTo(position)) {
+ if (mCursor != null && moveCursorTo(position)) {
return mCursor;
} else {
return null;
@@ -152,7 +153,7 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
* Returns the row id associated with the specified position in the list.
*/
public long getItemId(int position) {
- if (mDataValid && moveCursorTo(position)) {
+ if (mCursor != null && moveCursorTo(position)) {
return mCursor.getString(mRowIDColumn).hashCode();
} else {
return 0;
@@ -180,10 +181,8 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
mCursor = newCursor;
if (newCursor != null) {
mRowIDColumn = newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
- mDataValid = true;
} else {
mRowIDColumn = -1;
- mDataValid = false;
}
setItemPosition();
@@ -231,7 +230,6 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
private void init(Context context, Cursor c) {
boolean cursorPresent = c != null;
mCursor = c;
- mDataValid = cursorPresent;
mContext = context;
mRowIDColumn = cursorPresent
? mCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI) : -1;
@@ -242,7 +240,7 @@ public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
* row id to cursor position.
*/
private void setItemPosition() {
- if (!mDataValid || mCursor == null || mCursor.isClosed()) {
+ if (mCursor == null || mCursor.isClosed()) {
mItemPosition = null;
return;
}
diff --git a/photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java b/photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java
index 9c24575..2065b2a 100644
--- a/photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java
+++ b/photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java
@@ -86,6 +86,10 @@ public abstract class BaseFragmentPagerAdapter extends PagerAdapter {
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
+ if(fragment == null) {
+ if (DEBUG) Log.e(TAG, "NPE workaround for getItem(). See b/7103023");
+ return null;
+ }
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), position));
diff --git a/photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java b/photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java
index bf75ecb..4536432 100644
--- a/photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java
+++ b/photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java
@@ -33,24 +33,38 @@ import com.android.ex.photo.provider.PhotoContract;
public class PhotoPagerAdapter extends BaseCursorPagerAdapter {
private int mContentUriIndex;
private int mThumbnailUriIndex;
+ private int mLoadingIndex;
+ private final float mMaxScale;
- public PhotoPagerAdapter(Context context, FragmentManager fm, Cursor c) {
+ public PhotoPagerAdapter(Context context, FragmentManager fm, Cursor c, float maxScale) {
super(context, fm, c);
+ mMaxScale = maxScale;
}
@Override
public Fragment getItem(Context context, Cursor cursor, int position) {
final String photoUri = cursor.getString(mContentUriIndex);
final String thumbnailUri = cursor.getString(mThumbnailUriIndex);
+ boolean loading;
+ if(mLoadingIndex != -1) {
+ loading = Boolean.valueOf(cursor.getString(mLoadingIndex));
+ } else {
+ loading = false;
+ }
+ boolean onlyShowSpinner = false;
+ if(photoUri == null && loading) {
+ onlyShowSpinner = true;
+ }
// create new PhotoViewFragment
final PhotoViewIntentBuilder builder =
Intents.newPhotoViewFragmentIntentBuilder(mContext);
builder
.setResolvedPhotoUri(photoUri)
- .setThumbnailUri(thumbnailUri);
+ .setThumbnailUri(thumbnailUri)
+ .setMaxInitialScale(mMaxScale);
- return new PhotoViewFragment(builder.build(), position, this);
+ return new PhotoViewFragment(builder.build(), position, this, onlyShowSpinner);
}
@Override
@@ -60,9 +74,12 @@ public class PhotoPagerAdapter extends BaseCursorPagerAdapter {
newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.CONTENT_URI);
mThumbnailUriIndex =
newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.THUMBNAIL_URI);
+ mLoadingIndex =
+ newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.LOADING_INDICATOR);
} else {
mContentUriIndex = -1;
mThumbnailUriIndex = -1;
+ mLoadingIndex = -1;
}
return super.swapCursor(newCursor);
diff --git a/photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java b/photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java
index efb8145..e62383a 100644
--- a/photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java
+++ b/photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java
@@ -106,6 +106,9 @@ public class PhotoViewFragment extends Fragment implements
/** Whether or not the fragment should make the photo full-screen */
private boolean mFullScreen;
+ /** Whether or not this fragment will only show the loading spinner */
+ private final boolean mOnlyShowSpinner;
+
/** Whether or not the progress bar is showing valid information about the progress stated */
private boolean mProgressBarNeeded = true;
@@ -113,13 +116,16 @@ public class PhotoViewFragment extends Fragment implements
public PhotoViewFragment() {
mPosition = -1;
+ mOnlyShowSpinner = false;
mProgressBarNeeded = true;
}
- public PhotoViewFragment(Intent intent, int position, PhotoPagerAdapter adapter) {
+ public PhotoViewFragment(Intent intent, int position, PhotoPagerAdapter adapter,
+ boolean onlyShowSpinner) {
mIntent = intent;
mPosition = position;
mAdapter = adapter;
+ mOnlyShowSpinner = onlyShowSpinner;
mProgressBarNeeded = true;
}
@@ -184,6 +190,7 @@ public class PhotoViewFragment extends Fragment implements
final View view = inflater.inflate(R.layout.photo_fragment_view, container, false);
mPhotoView = (PhotoView) view.findViewById(R.id.photo_view);
+ mPhotoView.setMaxInitialScale(mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1));
mPhotoView.setOnClickListener(this);
mPhotoView.setFullScreen(mFullScreen, false);
mPhotoView.enableImageTransforms(true);
@@ -245,6 +252,9 @@ public class PhotoViewFragment extends Fragment implements
@Override
public Loader<Bitmap> onCreateLoader(int id, Bundle args) {
+ if(mOnlyShowSpinner) {
+ return null;
+ }
switch (id) {
case LOADER_ID_PHOTO:
return new PhotoBitmapLoader(getActivity(), mResolvedPhotoUri);
@@ -269,28 +279,25 @@ public class PhotoViewFragment extends Fragment implements
bindPhoto(data);
mPhotoPreviewAndProgress.setVisibility(View.GONE);
mProgressBarNeeded = false;
+ } else {
+ // Received a null result for the full size image. Instead attempt to load the
+ // thumbnail
+ getLoaderManager().initLoader(LOADER_ID_THUMBNAIL, null, this);
}
break;
case LOADER_ID_THUMBNAIL:
- if (isPhotoBound()) {
+ mProgressBarNeeded = false;
+ if (isPhotoBound()) {
// There is need to do anything with the thumbnail image, as the full size
// image is being shown.
mPhotoPreviewAndProgress.setVisibility(View.GONE);
- mProgressBarNeeded = false;
return;
- } else {
- // Make the preview image view visible
+ } else if (data == null) {
+ // no preview, show default
mPhotoPreviewImage.setVisibility(View.VISIBLE);
-
- if (data == null) {
- // no preview, show default
- mPhotoPreviewImage.setImageResource(R.drawable.default_image);
- mPhotoPreviewImage.setScaleType(ImageView.ScaleType.CENTER);
- } else {
- // Show the preview
- mPhotoPreviewImage.setImageBitmap(data);
- }
- // Now load the full size image
+ mPhotoPreviewImage.setImageResource(R.drawable.default_image);
+ } else {
+ bindPhoto(data);
getLoaderManager().initLoader(LOADER_ID_PHOTO, null, this);
}
break;
@@ -348,10 +355,6 @@ public class PhotoViewFragment extends Fragment implements
// we're not in the foreground; reset our view
resetViews();
} else {
- if (!isPhotoBound()) {
- // Restart the loader
- getLoaderManager().restartLoader(LOADER_ID_THUMBNAIL, null, this);
- }
mCallback.onFragmentVisible(this);
}
}
diff --git a/photoviewer/src/com/android/ex/photo/provider/PhotoContract.java b/photoviewer/src/com/android/ex/photo/provider/PhotoContract.java
index 439b68b..8483620 100644
--- a/photoviewer/src/com/android/ex/photo/provider/PhotoContract.java
+++ b/photoviewer/src/com/android/ex/photo/provider/PhotoContract.java
@@ -48,6 +48,11 @@ public final class PhotoContract {
* This string column is the MIME type.
*/
public static final String CONTENT_TYPE = "contentType";
+ /**
+ * This boolean column indicates that a loading indicator should display permenantly
+ * if no image urls are provided.
+ */
+ public static final String LOADING_INDICATOR = "loadingIndicator";
}
diff --git a/photoviewer/src/com/android/ex/photo/util/ImageUtils.java b/photoviewer/src/com/android/ex/photo/util/ImageUtils.java
index 7fe971a..f37a1ad 100644
--- a/photoviewer/src/com/android/ex/photo/util/ImageUtils.java
+++ b/photoviewer/src/com/android/ex/photo/util/ImageUtils.java
@@ -31,10 +31,14 @@ import android.util.Log;
import com.android.ex.photo.PhotoViewActivity;
import com.android.ex.photo.util.Exif;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
/**
* Image utilities
@@ -90,12 +94,15 @@ public class ImageUtils {
* @return The new bitmap or null
*/
public static Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) {
+ // TODO: make this method not download the image for both getImageBounds and decodeStream
InputStream inputStream = null;
try {
final BitmapFactory.Options opts = new BitmapFactory.Options();
final Point bounds = getImageBounds(resolver, uri);
-
- inputStream = resolver.openInputStream(uri);
+ inputStream = openInputStream(resolver, uri);
+ if (bounds == null || inputStream == null) {
+ return null;
+ }
opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);
@@ -141,6 +148,7 @@ public class ImageUtils {
*/
public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
ByteArrayOutputStream out = null;
+ InputStream byteStream = null;
try {
out = new ByteArrayOutputStream();
final byte[] buffer = new byte[4096];
@@ -149,12 +157,16 @@ public class ImageUtils {
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);
- final Bitmap originalBitmap =
- BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length, opts);
+
+ // Create an InputStream from this byte array
+ byteStream = new ByteArrayInputStream(bitmapBytes);
+
+ final Bitmap originalBitmap = BitmapFactory.decodeStream(byteStream, outPadding, opts);
if (originalBitmap != null && orientation != 0) {
final Matrix matrix = new Matrix();
@@ -177,6 +189,13 @@ public class ImageUtils {
// Do nothing
}
}
+ if (byteStream != null) {
+ try {
+ byteStream.close();
+ } catch (IOException e) {
+ // Do nothing
+ }
+ }
}
}
@@ -192,10 +211,13 @@ public class ImageUtils {
throws IOException {
final BitmapFactory.Options opts = new BitmapFactory.Options();
InputStream inputStream = null;
-
+ String scheme = uri.getScheme();
try {
opts.inJustDecodeBounds = true;
- inputStream = resolver.openInputStream(uri);
+ inputStream = openInputStream(resolver, uri);
+ if (inputStream == null) {
+ return null;
+ }
decodeStream(inputStream, null, opts);
return new Point(opts.outWidth, opts.outHeight);
@@ -208,4 +230,22 @@ public class ImageUtils {
}
}
}
+
+ private static InputStream openInputStream(ContentResolver resolver, Uri uri) throws
+ FileNotFoundException {
+ String scheme = uri.getScheme();
+ if("http".equals(scheme) || "https".equals(scheme)) {
+ 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());
+ return null;
+ }
+ }
+ return resolver.openInputStream(uri);
+ }
}
diff --git a/photoviewer/src/com/android/ex/photo/views/PhotoView.java b/photoviewer/src/com/android/ex/photo/views/PhotoView.java
index 8575bb1..838a60d 100644
--- a/photoviewer/src/com/android/ex/photo/views/PhotoView.java
+++ b/photoviewer/src/com/android/ex/photo/views/PhotoView.java
@@ -108,6 +108,8 @@ public class PhotoView extends View implements OnGestureListener,
private Rect mCropRect = new Rect();
/** Actual crop size; may differ from {@link #sCropSize} if the screen is smaller */
private int mCropSize;
+ /** The maximum amount of scaling to apply to images */
+ private float mMaxInitialScaleFactor;
/** Gesture detector */
private GestureDetectorCompat mGestureDetector;
@@ -699,9 +701,13 @@ public class PhotoView extends View implements OnGestureListener,
} else {
mTempDst.set(0, 0, vwidth, vheight);
}
-
- if (dwidth < vwidth && dheight < vheight && !mAllowCrop) {
- mMatrix.setTranslate(vwidth / 2 - dwidth / 2, vheight / 2 - dheight / 2);
+ RectF scaledDestination = new RectF(
+ (vwidth / 2) - (dwidth * mMaxInitialScaleFactor / 2),
+ (vheight / 2) - (dheight * mMaxInitialScaleFactor / 2),
+ (vwidth / 2) + (dwidth * mMaxInitialScaleFactor / 2),
+ (vheight / 2) + (dheight * mMaxInitialScaleFactor / 2));
+ if(mTempDst.contains(scaledDestination)) {
+ mMatrix.setRectToRect(mTempSrc, scaledDestination, Matrix.ScaleToFit.CENTER);
} else {
mMatrix.setRectToRect(mTempSrc, mTempDst, Matrix.ScaleToFit.CENTER);
}
@@ -1285,4 +1291,8 @@ public class PhotoView extends View implements OnGestureListener,
mHeader.post(this);
}
}
+
+ public void setMaxInitialScale(float f) {
+ mMaxInitialScaleFactor = f;
+ }
}