Messaging: Vendor chips, colorpicker, photoviewer, and vcard in-tree

Clone no longer depends on LineageOS gitlinks; the libraries used by the app live in this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-29 06:19:53 +09:00
co-authored by Cursor
parent a6b23f0f3c
commit 49789fc4a5
239 changed files with 35551 additions and 4 deletions
@@ -0,0 +1,47 @@
package com.android.ex.photo;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
/**
* Wrapper activity for an action bar. This wraps either a {@link android.app.ActionBar} or
* {@link androidx.appcompat.app.ActionBar}.
*/
public interface ActionBarInterface {
public interface OnMenuVisibilityListener {
/**
* Called when an action bar menu is shown or hidden. Applications may want to use
* this to tune auto-hiding behavior for the action bar or pause/resume video playback,
* gameplay, or other activity within the main content area.
*
* @param isVisible True if an action bar menu is now visible, false if no action bar
* menus are visible.
*/
public void onMenuVisibilityChanged(boolean isVisible);
}
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp);
public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener);
/**
* Wrapper for {@code setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE,
* ActionBar.DISPLAY_SHOW_TITLE)}.
*/
public void setDisplayOptionsShowTitle();
@Nullable
public CharSequence getTitle();
public void setTitle(@Nullable CharSequence title);
public void setSubtitle(@Nullable CharSequence subtitle);
public void show();
public void hide();
public void setLogo(@Nullable Drawable logo);
}
@@ -0,0 +1,351 @@
/*
* 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.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
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";
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_CONTENT_DESCRIPTION = "content_description";
public static final String EXTRA_MAX_INITIAL_SCALE = "max_scale";
public static final String EXTRA_WATCH_NETWORK = "watch_network";
public static final String EXTRA_ENABLE_TIMER_LIGHTS_OUT = "enable_timer_lights_out";
// 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.
*
* @param context The context
* @return The intent builder
*/
public static PhotoViewIntentBuilder newPhotoViewActivityIntentBuilder(Context context) {
return new PhotoViewIntentBuilder(context, PhotoViewActivity.class);
}
/**
* Gets a photo view intent builder to display the photo view fragment
*
* @param context The context
* @return The intent builder
*/
public static PhotoViewIntentBuilder newPhotoViewFragmentIntentBuilder(Context context) {
return newPhotoViewFragmentIntentBuilder(context, PhotoViewFragment.class);
}
/**
* Gets a photo view intent builder to display the photo view fragment with a custom fragment
* subclass.
*
* @param context The context
* @param clazz Subclass of PhotoViewFragment to use
* @return The intent builder
*/
public static PhotoViewIntentBuilder newPhotoViewFragmentIntentBuilder(Context context,
Class<? extends PhotoViewFragment> clazz) {
return new PhotoViewIntentBuilder(context, clazz);
}
/** Gets a new photo view intent builder */
public static PhotoViewIntentBuilder newPhotoViewIntentBuilder(
Context context, Class<? extends Activity> cls) {
return new PhotoViewIntentBuilder(context, cls);
}
/** Gets a new photo view intent builder */
public static PhotoViewIntentBuilder newPhotoViewIntentBuilder(
Context context, String activityName) {
return new PhotoViewIntentBuilder(context, activityName);
}
/** Builder to create a photo view intent */
public static class PhotoViewIntentBuilder {
private final Intent mIntent;
/** 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 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 */
private String mResolvedPhotoUri;
/** The projection for the query to use; optional */
private String[] mProjection;
/** The URI of a thumbnail of the photo to display */
private String mThumbnailUri;
/** The content Description for the photo to show */
private String mContentDescription;
/** The maximum scale to display images at before */
private Float mMaxInitialScale;
/** True if lights out should automatically be invoked on a timer basis */
private boolean mEnableTimerLightsOut;
/**
* 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);
initialize();
}
private PhotoViewIntentBuilder(Context context, String activityName) {
mIntent = new Intent();
mIntent.setClassName(context, activityName);
initialize();
}
private void initialize() {
mScaleAnimation = false;
mActionBarHiddenInitially = false;
mDisplayFullScreenThumbs = false;
mEnableTimerLightsOut = true;
}
/** Sets auto lights out */
public PhotoViewIntentBuilder setEnableTimerLightsOut(boolean enable) {
mEnableTimerLightsOut = enable;
return this;
}
/** Sets the photo index */
public PhotoViewIntentBuilder setPhotoIndex(Integer photoIndex) {
mPhotoIndex = photoIndex;
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;
return this;
}
/** Sets the query projection */
public PhotoViewIntentBuilder setProjection(String[] projection) {
mProjection = projection;
return this;
}
/** Sets the resolved photo URI. This method is for the case
* where the URI given to {@link PhotoViewActivity} points directly
* to a single image and does not need to be resolved via a query
* to the {@link ContentProvider}. If this value is set, it supersedes
* {@link #setPhotosUri(String)}. */
public PhotoViewIntentBuilder setResolvedPhotoUri(String resolvedPhotoUri) {
mResolvedPhotoUri = resolvedPhotoUri;
return this;
}
/**
* Sets the URI for a thumbnail preview of the photo.
*/
public PhotoViewIntentBuilder setThumbnailUri(String thumbnailUri) {
mThumbnailUri = thumbnailUri;
return this;
}
/**
* Sets the content Description for the photo
*/
public PhotoViewIntentBuilder setContentDescription(String contentDescription) {
mContentDescription = contentDescription;
return this;
}
/**
* Sets the maximum scale which an image is initially displayed at
*/
public PhotoViewIntentBuilder setMaxInitialScale(float maxScale) {
mMaxInitialScale = maxScale;
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;
}
/**
* Enable a scale animation that animates the initial photo URI passed in using
* {@link #setInitialPhotoUri}.
*
* Note: To avoid janky transitions, particularly when exiting the photoviewer, ensure the
* following system UI flags are set on the root view of the relying app's activity
* (via @{link View.setSystemUiVisibility(int)}):
* {@code View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE}
* As well, client should ensure {@code android:fitsSystemWindows} is set on the root
* content view.
*/
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);
// In Lollipop, each list of photos should appear as a document in the "Recents"
// list. In earlier versions, this flag was Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET.
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
// FLAG_ACTIVITY_CLEAR_TOP is needed for the case where the app tries to
// display a different photo while there is an existing activity instance
// for that list of photos. Since the initial photo is specified as an
// extra, without FLAG_ACTIVITY_CLEAR_TOP, the activity instance would
// just get restarted and it would display whatever photo it was last
// displaying. FLAG_ACTIVITY_CLEAR_TOP causes a new instance to be created,
// and it will display the new initial photo.
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (mPhotoIndex != null) {
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);
mIntent.setData(Uri.parse(mPhotosUri));
}
if (mResolvedPhotoUri != null) {
mIntent.putExtra(EXTRA_RESOLVED_PHOTO_URI, mResolvedPhotoUri);
}
if (mProjection != null) {
mIntent.putExtra(EXTRA_PROJECTION, mProjection);
}
if (mThumbnailUri != null) {
mIntent.putExtra(EXTRA_THUMBNAIL_URI, mThumbnailUri);
}
if (mContentDescription != null) {
mIntent.putExtra(EXTRA_CONTENT_DESCRIPTION, mContentDescription);
}
if (mMaxInitialScale != null) {
mIntent.putExtra(EXTRA_MAX_INITIAL_SCALE, mMaxInitialScale);
}
mIntent.putExtra(EXTRA_WATCH_NETWORK, mWatchNetwork);
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);
mIntent.putExtra(EXTRA_ENABLE_TIMER_LIGHTS_OUT, mEnableTimerLightsOut);
return mIntent;
}
}
}
@@ -0,0 +1,101 @@
package com.android.ex.photo;
import android.database.Cursor;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.loader.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.
*/
public static interface OnScreenListener {
/**
* The full screen state has changed.
*/
public void onFullScreenChanged(boolean fullScreen);
/**
* A new view has been activated and the previous view de-activated.
*/
public void onViewActivated();
/**
* This view is a candidate for being the next view.
*
* This will be called when the view is focused completely on the view immediately before
* or after this one, so that this view can reset itself if nessecary.
*/
public void onViewUpNext();
/**
* Called when a right-to-left touch move intercept is about to occur.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
* @return {@code true} if the touch should be intercepted.
*/
public boolean onInterceptMoveLeft(float origX, float origY);
/**
* Called when a left-to-right touch move intercept is about to occur.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
* @return {@code true} if the touch should be intercepted.
*/
public boolean onInterceptMoveRight(float origX, float origY);
}
public static interface CursorChangedListener {
/**
* Called when the cursor that contains the photo list data
* is updated. Note that there is no guarantee that the cursor
* will be at the proper position.
* @param cursor the cursor containing the photo list data
*/
public void onCursorChanged(Cursor cursor);
}
public void addScreenListener(int position, OnScreenListener listener);
public void removeScreenListener(int position);
public void addCursorListener(CursorChangedListener listener);
public void removeCursorListener(CursorChangedListener listener);
public void setViewActivated(int position);
public void onNewPhotoLoaded(int position);
public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment,
boolean success);
public void toggleFullScreen();
public boolean isFragmentActive(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.
*/
public PhotoPagerAdapter getAdapter();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,189 @@
/*
* 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.content.Context;
import androidx.core.view.MotionEventCompat;
import androidx.viewpager.widget.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
/**
* View pager for photo view fragments. Define our own class so we can specify the
* view pager in XML.
*/
public class PhotoViewPager extends ViewPager {
/**
* A type of intercept that should be performed
*/
public static enum InterceptType { NONE, LEFT, RIGHT, BOTH }
/**
* Provides an ability to intercept touch events.
* <p>
* {@link ViewPager} intercepts all touch events and we need to be able to override this
* behavior. Instead, we could perform a similar function by declaring a custom
* {@link android.view.ViewGroup} to contain the pager and intercept touch events at a higher
* level.
*/
public static interface OnInterceptTouchListener {
/**
* Called when a touch intercept is about to occur.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
* @return Which type of touch, if any, should should be intercepted.
*/
public InterceptType onTouchIntercept(float origX, float origY);
}
private static final int INVALID_POINTER = -1;
private float mLastMotionX;
private int mActivePointerId;
/** The x coordinate where the touch originated */
private float mActivatedX;
/** The y coordinate where the touch originated */
private float mActivatedY;
private OnInterceptTouchListener mListener;
public PhotoViewPager(Context context) {
super(context);
initialize();
}
public PhotoViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
private void initialize() {
// Set the page transformer to perform the transition animation
// for each page in the view.
setPageTransformer(true, new PageTransformer() {
@Override
public void transformPage(View page, float position) {
// The >= 1 is needed so that the page
// (page A) that transitions behind the newly visible
// page (page B) that comes in from the left does not
// get the touch events because it is still on screen
// (page A is still technically on screen despite being
// invisible). This makes sure that when the transition
// has completely finished, we revert it to its default
// behavior and move it off of the screen.
if (position < 0 || position >= 1.f) {
page.setTranslationX(0);
page.setAlpha(1.f);
page.setScaleX(1);
page.setScaleY(1);
} else {
page.setTranslationX(-position * page.getWidth());
page.setAlpha(Math.max(0,1.f - position));
final float scale = Math.max(0, 1.f - position * 0.3f);
page.setScaleX(scale);
page.setScaleY(scale);
}
}
});
}
/**
* {@inheritDoc}
* <p>
* We intercept touch event intercepts so we can prevent switching views when the
* current view is internally scrollable.
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final InterceptType intercept = (mListener != null)
? mListener.onTouchIntercept(mActivatedX, mActivatedY)
: InterceptType.NONE;
final boolean ignoreScrollLeft =
(intercept == InterceptType.BOTH || intercept == InterceptType.LEFT);
final boolean ignoreScrollRight =
(intercept == InterceptType.BOTH || intercept == InterceptType.RIGHT);
// Only check ability to page if we can't scroll in one / both directions
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = INVALID_POINTER;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (ignoreScrollLeft || ignoreScrollRight) {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex =
MotionEventCompat.findPointerIndex(ev, activePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
if (ignoreScrollLeft && ignoreScrollRight) {
mLastMotionX = x;
return false;
} else if (ignoreScrollLeft && (x > mLastMotionX)) {
mLastMotionX = x;
return false;
} else if (ignoreScrollRight && (x < mLastMotionX)) {
mLastMotionX = x;
return false;
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
mLastMotionX = ev.getX();
// Use the raw x/y as the children can be located anywhere and there isn't a
// single offset that would be meaningful
mActivatedX = ev.getRawX();
mActivatedY = ev.getRawY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEventCompat.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
// Our active pointer going up; select a new active pointer
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
}
break;
}
}
return super.onInterceptTouchEvent(ev);
}
/**
* sets the intercept touch listener.
*/
public void setOnInterceptTouchListener(OnInterceptTouchListener l) {
mListener = l;
}
}
@@ -0,0 +1,259 @@
/*
* 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.adapters;
import android.content.Context;
import android.database.Cursor;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import com.android.ex.photo.provider.PhotoContract;
import java.util.HashMap;
/**
* Page adapter for use with an BaseCursorLoader. Unlike other cursor adapters, this has no
* observers for automatic refresh. Instead, it depends upon external mechanisms to provide
* the update signal.
*/
public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
private static final String TAG = "BaseCursorPagerAdapter";
protected Context mContext;
protected Cursor mCursor;
protected int mRowIDColumn;
/** Mapping of row ID to cursor position */
protected SparseIntArray mItemPosition;
/** Mapping of instantiated object to row ID */
protected final HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
/**
* Constructor that always enables auto-requery.
*
* @param c The cursor from which to get the data.
* @param context The context
*/
public BaseCursorPagerAdapter(Context context, FragmentManager fm, Cursor c) {
super(fm);
init(context, c);
}
/**
* Makes a fragment for the data pointed to by the cursor
*
* @param context Interface to application's global information
* @param cursor The cursor from which to get the data. The cursor is already
* moved to the correct position.
* @return the newly created fragment.
*/
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 (mCursor != null && moveCursorTo(position)) {
return getItem(mContext, mCursor, position);
}
return null;
}
@Override
public int getCount() {
if (mCursor != null) {
return mCursor.getCount();
} else {
return 0;
}
}
@Override
public Object instantiateItem(View container, int position) {
if (mCursor == null) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
final Integer rowId;
if (moveCursorTo(position)) {
rowId = mCursor.getString(mRowIDColumn).hashCode();
} else {
rowId = null;
}
// Create the fragment and bind cursor data
final Object obj = super.instantiateItem(container, position);
if (obj != null) {
mObjectRowMap.put(obj, rowId);
}
return obj;
}
@Override
public void destroyItem(View container, int position, Object object) {
mObjectRowMap.remove(object);
super.destroyItem(container, position, object);
}
@Override
public int getItemPosition(Object object) {
final Integer rowId = mObjectRowMap.get(object);
if (rowId == null || mItemPosition == null) {
return POSITION_NONE;
}
final int position = mItemPosition.get(rowId, POSITION_NONE);
return position;
}
/**
* @return true if data is valid
*/
public boolean isDataValid() {
return mCursor != null;
}
/**
* Returns the cursor.
*/
public Cursor getCursor() {
return mCursor;
}
/**
* Returns the data item associated with the specified position in the data set.
*/
public Object getDataItem(int position) {
if (mCursor != null && moveCursorTo(position)) {
return mCursor;
} else {
return null;
}
}
/**
* Returns the row id associated with the specified position in the list.
*/
public long getItemId(int position) {
if (mCursor != null && moveCursorTo(position)) {
return mCursor.getString(mRowIDColumn).hashCode();
} else {
return 0;
}
}
/**
* Swap in a new Cursor, returning the old Cursor.
*
* @param newCursor The new cursor to be used.
* @return Returns the previously set Cursor, or null if there was not one.
* If the given new Cursor is the same instance is the previously set
* Cursor, null is also returned.
*/
public Cursor swapCursor(Cursor newCursor) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "swapCursor old=" + (mCursor == null ? -1 : mCursor.getCount()) +
"; new=" + (newCursor == null ? -1 : newCursor.getCount()));
}
if (newCursor == mCursor) {
return null;
}
Cursor oldCursor = mCursor;
mCursor = newCursor;
if (newCursor != null) {
mRowIDColumn = newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
} else {
mRowIDColumn = -1;
}
setItemPosition();
notifyDataSetChanged(); // notify the observers about the new cursor
return oldCursor;
}
/**
* Converts the cursor into a CharSequence. Subclasses should override this
* method to convert their results. The default implementation returns an
* empty String for null values or the default String representation of
* the value.
*
* @param cursor the cursor to convert to a CharSequence
* @return a CharSequence representing the value
*/
public CharSequence convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
@Override
protected String makeFragmentName(int viewId, int index) {
if (moveCursorTo(index)) {
return "android:pager:" + viewId + ":" + mCursor.getString(mRowIDColumn).hashCode();
} else {
return super.makeFragmentName(viewId, index);
}
}
/**
* Moves the cursor to the given position
*
* @return {@code true} if the cursor's position was set. Otherwise, {@code false}.
*/
private boolean moveCursorTo(int position) {
if (mCursor != null && !mCursor.isClosed()) {
return mCursor.moveToPosition(position);
}
return false;
}
/**
* Initialize the adapter.
*/
private void init(Context context, Cursor c) {
boolean cursorPresent = c != null;
mCursor = c;
mContext = context;
mRowIDColumn = cursorPresent
? mCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI) : -1;
}
/**
* Sets the {@link #mItemPosition} instance variable with the current mapping of
* row id to cursor position.
*/
private void setItemPosition() {
if (mCursor == null || mCursor.isClosed()) {
mItemPosition = null;
return;
}
SparseIntArray itemPosition = new SparseIntArray(mCursor.getCount());
mCursor.moveToPosition(-1);
while (mCursor.moveToNext()) {
final int rowId = mCursor.getString(mRowIDColumn).hashCode();
final int position = mCursor.getPosition();
itemPosition.append(rowId, position);
}
mItemPosition = itemPosition;
}
}
@@ -0,0 +1,191 @@
/*
* 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.adapters;
import android.os.Parcelable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.collection.LruCache;
import androidx.viewpager.widget.PagerAdapter;
import android.util.Log;
import android.view.View;
/**
* NOTE: This is a direct copy of {@link androidx.fragment.app.FragmentPagerAdapter}
* with four very important modifications.
* <p>
* <ol>
* <li>The method {@link #makeFragmentName(int, int)} is declared "protected"
* in our class. We need to be able to re-define the fragment's name according to data
* only available to sub-classes.</li>
* <li>The method {@link #isViewFromObject(View, Object)} has been reimplemented to search
* the entire view hierarchy for the given view.</li>
* <li>In method {@link #destroyItem(View, int, Object)}, the fragment is detached and
* added to a cache. If the fragment is evicted from the cache, it will be deleted.
* An album may contain thousands of photos and we want to avoid having thousands of
* fragments.</li>
* </ol>
*/
public abstract class BaseFragmentPagerAdapter extends PagerAdapter {
/** The default size of {@link #mFragmentCache} */
private static final int DEFAULT_CACHE_SIZE = 5;
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;
/** A cache to store detached fragments before they are removed */
private LruCache<String, Fragment> mFragmentCache = new FragmentCache(DEFAULT_CACHE_SIZE);
public BaseFragmentPagerAdapter(androidx.fragment.app.FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
@Override
public void startUpdate(View container) {
}
@Override
public Object instantiateItem(View container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), position);
// Remove item from the cache
mFragmentCache.remove(name);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + position + ": f=" + fragment);
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));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
}
return fragment;
}
@Override
public void destroyItem(View container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
Fragment fragment = (Fragment) object;
String name = fragment.getTag();
if (name == null) {
// We prefer to get the name directly from the fragment, but, if the fragment is
// detached before the add transaction is committed, this could be 'null'. In
// that case, generate a name so we can still cache the fragment.
name = makeFragmentName(container.getId(), position);
}
mFragmentCache.put(name, fragment);
mCurTransaction.detach(fragment);
}
@Override
public void setPrimaryItem(View container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(View container) {
if (mCurTransaction != null && !mFragmentManager.isDestroyed()) {
mCurTransaction.commitAllowingStateLoss();
mCurTransaction = null;
mFragmentManager.executePendingTransactions();
}
}
@Override
public boolean isViewFromObject(View view, Object object) {
// Ascend the tree to determine if the view is a child of the fragment
View root = ((Fragment) object).getView();
for (Object v = view; v instanceof View; v = ((View) v).getParent()) {
if (v == root) {
return true;
}
}
return false;
}
@Override
public Parcelable saveState() {
return null;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}
/** Creates a name for the fragment */
protected String makeFragmentName(int viewId, int index) {
return "android:switcher:" + viewId + ":" + index;
}
/**
* A cache of detached fragments.
*/
private class FragmentCache extends LruCache<String, Fragment> {
public FragmentCache(int size) {
super(size);
}
@Override
protected void entryRemoved(boolean evicted, String key,
Fragment oldValue, Fragment newValue) {
// remove the fragment if it's evicted OR it's replaced by a new fragment
if (evicted || (newValue != null && oldValue != newValue)) {
mCurTransaction.remove(oldValue);
}
}
}
}
@@ -0,0 +1,134 @@
/*
* 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.adapters;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import androidx.fragment.app.Fragment;
import androidx.collection.SimpleArrayMap;
import com.android.ex.photo.Intents;
import com.android.ex.photo.Intents.PhotoViewIntentBuilder;
import com.android.ex.photo.fragments.PhotoViewFragment;
import com.android.ex.photo.provider.PhotoContract.PhotoViewColumns;
import com.android.ex.photo.provider.PhotoContract.PhotoQuery;
/**
* Pager adapter for the photo view
*/
public class PhotoPagerAdapter extends BaseCursorPagerAdapter {
protected SimpleArrayMap<String, Integer> mColumnIndices =
new SimpleArrayMap<String, Integer>(PhotoQuery.PROJECTION.length);
protected final float mMaxScale;
protected boolean mDisplayThumbsFullScreen;
public PhotoPagerAdapter(
Context context, androidx.fragment.app.FragmentManager fm, Cursor c,
float maxScale, boolean thumbsFullScreen) {
super(context, fm, c);
mMaxScale = maxScale;
mDisplayThumbsFullScreen = thumbsFullScreen;
}
@Override
public Fragment getItem(Context context, Cursor cursor, int position) {
final String photoUri = getPhotoUri(cursor);
final String thumbnailUri = getThumbnailUri(cursor);
final String contentDescription = getPhotoName(cursor);
boolean loading = shouldShowLoadingIndicator(cursor);
boolean onlyShowSpinner = false;
if(photoUri == null && loading) {
onlyShowSpinner = true;
}
// create new PhotoViewFragment
final PhotoViewIntentBuilder builder =
Intents.newPhotoViewFragmentIntentBuilder(mContext, getPhotoViewFragmentClass());
builder
.setResolvedPhotoUri(photoUri)
.setThumbnailUri(thumbnailUri)
.setContentDescription(contentDescription)
.setDisplayThumbsFullScreen(mDisplayThumbsFullScreen)
.setMaxInitialScale(mMaxScale);
return createPhotoViewFragment(builder.build(), position, onlyShowSpinner);
}
protected Class<? extends PhotoViewFragment> getPhotoViewFragmentClass() {
return PhotoViewFragment.class;
}
protected PhotoViewFragment createPhotoViewFragment(
Intent intent, int position, boolean onlyShowSpinner) {
return PhotoViewFragment.newInstance(intent, position, onlyShowSpinner);
}
@Override
public Cursor swapCursor(Cursor newCursor) {
mColumnIndices.clear();
if (newCursor != null) {
for(String column : PhotoQuery.PROJECTION) {
mColumnIndices.put(column, newCursor.getColumnIndexOrThrow(column));
}
for(String column : PhotoQuery.OPTIONAL_COLUMNS) {
int index = newCursor.getColumnIndex(column);
if (index != -1) {
mColumnIndices.put(column, index);
}
}
}
return super.swapCursor(newCursor);
}
public String getPhotoUri(Cursor cursor) {
return getString(cursor, PhotoViewColumns.CONTENT_URI);
}
public String getThumbnailUri(Cursor cursor) {
return getString(cursor, PhotoViewColumns.THUMBNAIL_URI);
}
public String getContentType(Cursor cursor) {
return getString(cursor, PhotoViewColumns.CONTENT_TYPE);
}
public String getPhotoName(Cursor cursor) {
return getString(cursor, PhotoViewColumns.NAME);
}
public boolean shouldShowLoadingIndicator(Cursor cursor) {
String value = getString(cursor, PhotoViewColumns.LOADING_INDICATOR);
if (value == null) {
return false;
} else {
return Boolean.parseBoolean(value);
}
}
private String getString(Cursor cursor, String column) {
if (mColumnIndices.containsKey(column)) {
return cursor.getString(mColumnIndices.get(column));
} else {
return null;
}
}
}
@@ -0,0 +1,606 @@
/*
* 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.fragments;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
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.PhotoViewController.ActivityInterface;
import com.android.ex.photo.R;
import com.android.ex.photo.adapters.PhotoPagerAdapter;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
import com.android.ex.photo.views.PhotoView;
import com.android.ex.photo.views.ProgressBarWrapper;
/**
* Displays a photo.
*/
public class PhotoViewFragment extends Fragment implements
LoaderManager.LoaderCallbacks<BitmapResult>,
OnClickListener,
OnScreenListener,
CursorChangedListener {
/**
* Interface for components that are internally scrollable left-to-right.
*/
public static interface HorizontallyScrollable {
/**
* Return {@code true} if the component needs to receive right-to-left
* touch movements.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
*/
public boolean interceptMoveLeft(float origX, float origY);
/**
* Return {@code true} if the component needs to receive left-to-right
* touch movements.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
*/
public boolean interceptMoveRight(float origX, float origY);
}
protected final static String STATE_INTENT_KEY =
"com.android.mail.photo.fragments.PhotoViewFragment.INTENT";
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 URL of a photo to display */
protected String mResolvedPhotoUri;
protected String mThumbnailUri;
protected String mContentDescription;
/** The intent we were launched with */
protected Intent mIntent;
protected PhotoViewCallbacks mCallback;
protected PhotoPagerAdapter mAdapter;
protected BroadcastReceiver mInternetStateReceiver;
protected PhotoView mPhotoView;
protected ImageView mPhotoPreviewImage;
protected TextView mEmptyText;
protected ImageView mRetryButton;
protected ProgressBarWrapper mPhotoProgressBar;
protected int mPosition;
/** 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;
/** Whether or not the progress bar is showing valid information about the progress stated */
protected boolean mProgressBarNeeded = true;
protected View mPhotoPreviewAndProgress;
protected boolean mThumbnailShown;
/** 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.
}
/**
* Create a {@link PhotoViewFragment}.
* @param intent
* @param position
* @param onlyShowSpinner
*/
public static PhotoViewFragment newInstance(
Intent intent, int position, boolean onlyShowSpinner) {
final PhotoViewFragment f = new PhotoViewFragment();
initializeArguments(intent, position, onlyShowSpinner, f);
return f;
}
public static void initializeArguments(
Intent intent, int position, boolean onlyShowSpinner, PhotoViewFragment f) {
final Bundle b = new Bundle();
b.putParcelable(ARG_INTENT, intent);
b.putInt(ARG_POSITION, position);
b.putBoolean(ARG_SHOW_SPINNER, onlyShowSpinner);
f.setArguments(b);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mCallback = getCallbacks();
if (mCallback == null) {
throw new IllegalArgumentException(
"Activity must be a derived class of PhotoViewActivity");
}
mAdapter = mCallback.getAdapter();
if (mAdapter == null) {
throw new IllegalStateException("Callback reported null adapter");
}
// Don't call until we've setup the entire view
setViewVisibility();
}
protected PhotoViewCallbacks getCallbacks() {
return ((ActivityInterface) getActivity()).getController();
}
@Override
public void onDetach() {
mCallback = null;
super.onDetach();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle bundle = getArguments();
if (bundle == null) {
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;
if (savedInstanceState != null) {
final Bundle state = savedInstanceState.getBundle(STATE_INTENT_KEY);
if (state != null) {
mIntent = new Intent().putExtras(state);
}
}
if (mIntent != null) {
mResolvedPhotoUri = mIntent.getStringExtra(Intents.EXTRA_RESOLVED_PHOTO_URI);
mThumbnailUri = mIntent.getStringExtra(Intents.EXTRA_THUMBNAIL_URI);
mContentDescription = mIntent.getStringExtra(Intents.EXTRA_CONTENT_DESCRIPTION);
mWatchNetworkState = mIntent.getBooleanExtra(Intents.EXTRA_WATCH_NETWORK, false);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.photo_fragment_view, container, false);
initializeView(view);
return view;
}
protected void initializeView(View view) {
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(false);
mPhotoView.setContentDescription(mContentDescription);
mPhotoPreviewAndProgress = view.findViewById(R.id.photo_preview);
mPhotoPreviewImage = (ImageView) view.findViewById(R.id.photo_preview_image);
mThumbnailShown = false;
final ProgressBar indeterminate =
(ProgressBar) view.findViewById(R.id.indeterminate_progress);
final ProgressBar determinate =
(ProgressBar) view.findViewById(R.id.determinate_progress);
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
public void onResume() {
super.onResume();
mCallback.addScreenListener(mPosition, this);
mCallback.addCursorListener(this);
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();
if (activeNetInfo != null) {
mConnected = activeNetInfo.isConnected();
} else {
// Best to set this to false, since it won't stop us from trying to download,
// only allow us to try re-download if we get notified that we do have a connection.
mConnected = false;
}
}
if (!isPhotoBound()) {
mProgressBarNeeded = true;
mPhotoPreviewAndProgress.setVisibility(View.VISIBLE);
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);
super.onPause();
}
@Override
public void onDestroyView() {
// Clean up views and other components
if (mPhotoView != null) {
mPhotoView.clear();
mPhotoView = null;
}
super.onDestroyView();
}
public String getPhotoUri() {
return mResolvedPhotoUri;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mIntent != null) {
outState.putParcelable(STATE_INTENT_KEY, mIntent.getExtras());
}
}
@Override
public Loader<BitmapResult> onCreateLoader(int id, Bundle args) {
if(mOnlyShowSpinner) {
return null;
}
String uri = null;
switch (id) {
case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL:
uri = mThumbnailUri;
break;
case PhotoViewCallbacks.BITMAP_LOADER_PHOTO:
uri = mResolvedPhotoUri;
break;
}
return mCallback.onCreateBitmapLoader(id, args, uri);
}
@Override
public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) {
// If we don't have a view, the fragment has been paused. We'll get the cursor again later.
// If we're not added, the fragment has detached during the loading process. We no longer
// need the result.
if (getView() == null || !isAdded()) {
return;
}
final Drawable data = result.getDrawable(getResources());
final int id = loader.getId();
switch (id) {
case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL:
if (mDisplayThumbsFullScreen) {
displayPhoto(result);
} else {
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.setImageDrawable(data);
mThumbnailShown = true;
}
mPhotoPreviewImage.setVisibility(View.VISIBLE);
if (getResources().getBoolean(R.bool.force_thumbnail_no_scaling)) {
mPhotoPreviewImage.setScaleType(ImageView.ScaleType.CENTER);
}
enableImageTransforms(false);
}
break;
case PhotoViewCallbacks.BITMAP_LOADER_PHOTO:
displayPhoto(result);
break;
default:
break;
}
if (mProgressBarNeeded == false) {
// Hide the progress bar as it isn't needed anymore.
mPhotoProgressBar.setVisibility(View.GONE);
}
if (data != null) {
mCallback.onNewPhotoLoaded(mPosition);
}
setViewVisibility();
}
private void displayPhoto(BitmapResult result) {
if (result.status == BitmapResult.STATUS_EXCEPTION) {
mProgressBarNeeded = false;
mEmptyText.setText(R.string.failed);
mEmptyText.setVisibility(View.VISIBLE);
mCallback.onFragmentPhotoLoadComplete(this, false /* success */);
} else {
mEmptyText.setVisibility(View.GONE);
final Drawable data = result.getDrawable(getResources());
bindPhoto(data);
mCallback.onFragmentPhotoLoadComplete(this, true /* success */);
}
}
/**
* Binds an image to the photo view.
*/
private void bindPhoto(Drawable drawable) {
if (drawable != null) {
if (mPhotoView != null) {
mPhotoView.bindDrawable(drawable);
}
enableImageTransforms(true);
mPhotoPreviewAndProgress.setVisibility(View.GONE);
mProgressBarNeeded = false;
}
}
public Drawable getDrawable() {
return (mPhotoView != null ? mPhotoView.getDrawable() : null);
}
/**
* Enable or disable image transformations. When transformations are enabled, this view
* consumes all touch events.
*/
public void enableImageTransforms(boolean enable) {
mPhotoView.enableImageTransforms(enable);
}
@Override
public void onLoaderReset(Loader<BitmapResult> loader) {
// Do nothing
}
@Override
public void onClick(View v) {
mCallback.toggleFullScreen();
}
@Override
public void onFullScreenChanged(boolean fullScreen) {
setViewVisibility();
}
@Override
public void onViewUpNext() {
resetViews();
}
@Override
public void onViewActivated() {
if (!mCallback.isFragmentActive(this)) {
// we're not in the foreground; reset our view
resetViews();
} else {
if (!isPhotoBound()) {
// Restart the loader
getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
null, this);
}
mCallback.onFragmentVisible(this);
}
}
/**
* Reset the views to their default states
*/
public void resetViews() {
if (mPhotoView != null) {
mPhotoView.resetTransformations();
}
}
@Override
public boolean onInterceptMoveLeft(float origX, float origY) {
if (!mCallback.isFragmentActive(this)) {
// we're not in the foreground; don't intercept any touches
return false;
}
return (mPhotoView != null && mPhotoView.interceptMoveLeft(origX, origY));
}
@Override
public boolean onInterceptMoveRight(float origX, float origY) {
if (!mCallback.isFragmentActive(this)) {
// we're not in the foreground; don't intercept any touches
return false;
}
return (mPhotoView != null && mPhotoView.interceptMoveRight(origX, origY));
}
/**
* Returns {@code true} if a photo has been bound. Otherwise, returns {@code false}.
*/
public boolean isPhotoBound() {
return (mPhotoView != null && mPhotoView.isPhotoBound());
}
/**
* Sets view visibility depending upon whether or not we're in "full screen" mode.
*/
private void setViewVisibility() {
final boolean fullScreen = mCallback == null ? false : mCallback.isFragmentFullScreen(this);
setFullScreen(fullScreen);
}
/**
* Sets full-screen mode for the views.
*/
public void setFullScreen(boolean fullScreen) {
mFullScreen = fullScreen;
}
@Override
public void onCursorChanged(Cursor cursor) {
if (mAdapter == null) {
// The adapter is set in onAttach(), and is guaranteed to be non-null. We have magically
// received an onCursorChanged without attaching to an activity. Ignore this cursor
// 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(
PhotoViewCallbacks.BITMAP_LOADER_PHOTO);
if (fakePhotoLoader != null) {
final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakePhotoLoader;
mResolvedPhotoUri = mAdapter.getPhotoUri(cursor);
loader.setPhotoUri(mResolvedPhotoUri);
loader.forceLoad();
}
if (!mThumbnailShown) {
final Loader<BitmapResult> fakeThumbnailLoader = manager.getLoader(
PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL);
if (fakeThumbnailLoader != null) {
final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakeThumbnailLoader;
mThumbnailUri = mAdapter.getThumbnailUri(cursor);
loader.setPhotoUri(mThumbnailUri);
loader.forceLoad();
}
}
}
}
public int getPosition() {
return mPosition;
}
public ProgressBarWrapper getPhotoProgressBar() {
return mPhotoProgressBar;
}
public TextView getEmptyText() {
return mEmptyText;
}
public ImageView getRetryButton() {
return mRetryButton;
}
public boolean isProgressBarNeeded() {
return mProgressBarNeeded;
}
private class InternetStateBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// This is only created if we have the correct permissions, so
ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
if (activeNetInfo == null || !activeNetInfo.isConnected()) {
mConnected = false;
return;
}
if (mConnected == false && !isPhotoBound()) {
if (mThumbnailShown == false) {
getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
null, PhotoViewFragment.this);
}
getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO,
null, PhotoViewFragment.this);
mConnected = true;
mPhotoProgressBar.setVisibility(View.VISIBLE);
}
}
}
}
@@ -0,0 +1,173 @@
/*
* 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.loaders;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.loader.content.AsyncTaskLoader;
import android.util.DisplayMetrics;
import com.android.ex.photo.PhotoViewController;
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>
implements PhotoBitmapLoaderInterface {
private String mPhotoUri;
private Bitmap mBitmap;
public PhotoBitmapLoader(Context context, String photoUri) {
super(context);
mPhotoUri = photoUri;
}
@Override
public void setPhotoUri(String photoUri) {
mPhotoUri = photoUri;
}
@Override
public BitmapResult loadInBackground() {
BitmapResult result = new BitmapResult();
Context context = getContext();
if (context != null && mPhotoUri != null) {
final ContentResolver resolver = context.getContentResolver();
try {
result = ImageUtils.createLocalBitmap(resolver, Uri.parse(mPhotoUri),
PhotoViewController.sMaxPhotoSize);
if (result.bitmap != null) {
result.bitmap.setDensity(DisplayMetrics.DENSITY_MEDIUM);
}
} catch (UnsupportedOperationException ex) {
// We got image bytes, but unable to decode to a Bitmap
result.status = BitmapResult.STATUS_EXCEPTION;
}
}
return result;
}
/**
* Called when there is new data to deliver to the client. The
* super class will take care of delivering it; the implementation
* here just adds a little more logic.
*/
@Override
public void deliverResult(BitmapResult result) {
Bitmap bitmap = result != null ? result.bitmap : null;
if (isReset()) {
// An async query came in while the loader is stopped. We
// don't need the result.
if (bitmap != null) {
onReleaseResources(bitmap);
}
return;
}
Bitmap oldBitmap = mBitmap;
mBitmap = bitmap;
if (isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(result);
}
// At this point we can release the resources associated with
// 'oldBitmap' if needed; now that the new result is delivered we
// know that it is no longer in use.
if (oldBitmap != null && oldBitmap != bitmap && !oldBitmap.isRecycled()) {
onReleaseResources(oldBitmap);
}
}
/**
* Handles a request to start the Loader.
*/
@Override
protected void onStartLoading() {
if (mBitmap != null) {
// If we currently have a result available, deliver it
// immediately.
BitmapResult result = new BitmapResult();
result.status = BitmapResult.STATUS_SUCCESS;
result.bitmap = mBitmap;
deliverResult(result);
}
if (takeContentChanged() || mBitmap == null) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
/**
* Handles a request to stop the Loader.
*/
@Override protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
/**
* Handles a request to cancel a load.
*/
@Override
public void onCanceled(BitmapResult result) {
super.onCanceled(result);
// At this point we can release the resources associated with 'bitmap'
// if needed.
if (result != null) {
onReleaseResources(result.bitmap);
}
}
/**
* Handles a request to completely reset the Loader.
*/
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
// At this point we can release the resources associated with 'bitmap'
// if needed.
if (mBitmap != null) {
onReleaseResources(mBitmap);
mBitmap = null;
}
}
/**
* Helper function to take care of releasing resources associated
* with an actively loaded data set.
*/
protected void onReleaseResources(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
@@ -0,0 +1,46 @@
package com.android.ex.photo.loaders;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
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 Drawable drawable;
public Bitmap bitmap;
public int status;
/**
* Returns a drawable to be used in the {@link com.android.ex.photo.views.PhotoView}.
* Should return null if the drawable is not ready to be shown (for instance, if
* the underlying bitmap is null).
*/
public Drawable getDrawable(Resources resources) {
if (resources == null) {
throw new IllegalArgumentException("resources can not be null!");
}
if (drawable != null) {
return drawable;
}
// Don't create a new drawable if there's no bitmap. PhotoViewFragment regards
// a null drawable as a signal to keep showing the loading stuff.
// b/12348405.
if (bitmap == null) {
return null;
}
return new BitmapDrawable(resources, bitmap);
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.loaders;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import androidx.loader.content.CursorLoader;
import com.android.ex.photo.provider.PhotoContract;
/**
* Loader for a set of photo IDs.
*/
public class PhotoPagerLoader extends CursorLoader {
private final Uri mPhotosUri;
private final String[] mProjection;
public PhotoPagerLoader(
Context context, Uri photosUri, String[] projection) {
super(context);
mPhotosUri = photosUri;
mProjection = projection != null ? projection : PhotoContract.PhotoQuery.PROJECTION;
}
@Override
public Cursor loadInBackground() {
Cursor returnCursor = null;
final Uri loaderUri = mPhotosUri.buildUpon().appendQueryParameter(
PhotoContract.ContentTypeParameters.CONTENT_TYPE, "image/").build();
setUri(loaderUri);
setProjection(mProjection);
returnCursor = super.loadInBackground();
return returnCursor;
}
}
@@ -0,0 +1,82 @@
/*
* 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.provider;
import android.net.Uri;
import android.provider.OpenableColumns;
public final class PhotoContract {
/** Columns for the view */
public static interface PhotoViewColumns {
/**
* This column is a {@link Uri} that can be queried
* for this individual image (resulting cursor has one single row for this image).
*/
public static final String URI = "uri";
/**
* This column is a {@link String} that can be queried for this
* individual image to return a displayable name.
*/
public static final String NAME = OpenableColumns.DISPLAY_NAME;
/**
* This column is a {@link Uri} that points to the downloaded local file.
* Can be null.
*/
public static final String CONTENT_URI = "contentUri";
/**
* This column is a {@link Uri} that points to a thumbnail of the image
* that ideally is a local file.
* Can be null.
*/
public static final String THUMBNAIL_URI = "thumbnailUri";
/**
* This string column is the MIME type.
*/
public static final String CONTENT_TYPE = "contentType";
/**
* 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 {
/** Projection of the returned cursor */
public final static String[] PROJECTION = {
PhotoViewColumns.URI,
PhotoViewColumns.NAME,
PhotoViewColumns.CONTENT_URI,
PhotoViewColumns.THUMBNAIL_URI,
PhotoViewColumns.CONTENT_TYPE
};
public final static String[] OPTIONAL_COLUMNS = {
PhotoViewColumns.LOADING_INDICATOR
};
}
public static final class ContentTypeParameters {
/**
* Parameter used to specify which type of content to return.
* Allows multiple types to be specified.
*/
public static final String CONTENT_TYPE = "contentType";
private ContentTypeParameters() {}
}
}
@@ -0,0 +1,211 @@
/*
* 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.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.
* @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 (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) {
continue;
}
offset++;
// Check if the marker is SOI or TEM.
if (marker == 0xD8 || marker == 0x01) {
continue;
}
// 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 || !has(jpeg, byteSize, offset + length - 1)) {
Log.e(TAG, "Invalid length");
return 0;
}
// Break if the marker is EXIF in APP1.
if (marker == 0xE1 && length >= 8 &&
pack(jpeg, offset + 2, 4, false) == 0x45786966 &&
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
if (length > 8) {
// Identify the byte order.
int tag = pack(jpeg, offset, 4, false);
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
Log.e(TAG, "Invalid byte order");
return 0;
}
final boolean littleEndian = (tag == 0x49492A00);
// Get the offset and check if it is reasonable.
int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
if (count < 10 || count > length) {
Log.e(TAG, "Invalid offset");
return 0;
}
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?
final int orientation = pack(jpeg, offset + 8, 2, littleEndian);
switch (orientation) {
case 1:
return 0;
case 3:
return 180;
case 6:
return 90;
case 8:
return 270;
}
Log.i(TAG, "Unsupported orientation");
return 0;
}
offset += 12;
length -= 12;
// Loop ends.
jpeg.advanceTo(offset - readBackwards);
}
}
return 0;
}
private static int pack(final InputStreamBuffer bytes, int offset, int length,
final boolean littleEndian) {
int step = 1;
if (littleEndian) {
offset += length - 1;
step = -1;
}
int value = 0;
while (length-- > 0) {
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);
}
}
@@ -0,0 +1,280 @@
/*
* 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.util;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.util.Base64;
import android.util.Log;
import com.android.ex.photo.PhotoViewController;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Pattern;
/**
* Image utilities
*/
public class ImageUtils {
// Logging
private static final String TAG = "ImageUtils";
/** Minimum class memory class to use full-res photos */
private final static long MIN_NORMAL_CLASS = 32;
/** Minimum class memory class to use small photos */
private final static long MIN_SMALL_CLASS = 24;
private static final String BASE64_URI_PREFIX = "base64,";
private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*");
public static enum ImageSize {
EXTRA_SMALL,
SMALL,
NORMAL,
}
public static final ImageSize sUseImageSize;
static {
// On HC and beyond, assume devices are more capable
if (Build.VERSION.SDK_INT >= 11) {
sUseImageSize = ImageSize.NORMAL;
} else {
if (PhotoViewController.sMemoryClass >= MIN_NORMAL_CLASS) {
// We have plenty of memory; use full sized photos
sUseImageSize = ImageSize.NORMAL;
} else if (PhotoViewController.sMemoryClass >= MIN_SMALL_CLASS) {
// We have slight less memory; use smaller sized photos
sUseImageSize = ImageSize.SMALL;
} else {
// We have little memory; use very small sized photos
sUseImageSize = ImageSize.EXTRA_SMALL;
}
}
}
/**
* @return true if the MimeType type is image
*/
public static boolean isImageMimeType(String mimeType) {
return mimeType != null && mimeType.startsWith("image/");
}
/**
* 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)
* @return The new bitmap or 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 Point bounds = getImageBounds(factory);
if (bounds == null) {
result.status = BitmapResult.STATUS_EXCEPTION;
return result;
}
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;
} catch (FileNotFoundException exception) {
// Do nothing - the photo will appear to be missing
} catch (IOException exception) {
result.status = BitmapResult.STATUS_EXCEPTION;
} catch (IllegalArgumentException exception) {
// Do nothing - the photo will appear to be missing
} catch (SecurityException exception) {
result.status = BitmapResult.STATUS_EXCEPTION;
}
return result;
}
/**
* Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
* BitmapFactory.Options)} that returns {@code null} on {@link
* OutOfMemoryError}.
*
* @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.
* @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)
*/
public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding,
final BitmapFactory.Options opts) throws FileNotFoundException {
InputStream is = null;
try {
// Determine the orientation for this image
is = factory.createInputStream();
final int orientation = Exif.getOrientation(is, -1);
if (is != null) {
is.close();
}
// Decode the bitmap
is = factory.createInputStream();
final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts);
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);
return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
originalBitmap.getHeight(), matrix, true);
}
return originalBitmap;
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
return null;
} catch (IOException ioe) {
Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Do nothing
}
}
}
}
/**
* Gets the image bounds
*
* @param factory Used to create the InputStream.
*
* @return The image bounds
*/
private static Point getImageBounds(final InputStreamFactory factory)
throws IOException {
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
decodeStream(factory, null, opts);
return new Point(opts.outWidth, opts.outHeight);
}
private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver,
final Uri uri) {
final String scheme = uri.getScheme();
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();
}
}
return new ByteArrayInputStream(mData);
}
private byte[] parseDataUri(final Uri uri) {
final String ssp = uri.getSchemeSpecificPart();
try {
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;
}
}
}
}
@@ -0,0 +1,376 @@
/*
* 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;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
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;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
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;
}
}
@@ -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();
}
}
}
@@ -0,0 +1,68 @@
package com.android.ex.photo.util;
import android.content.Context;
import android.os.Build;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.core.view.accessibility.AccessibilityRecordCompat;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
public class Util {
public static boolean isTouchExplorationEnabled(AccessibilityManager accessibilityManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return accessibilityManager.isTouchExplorationEnabled();
} else {
return false;
}
}
/**
* Make an announcement which is related to some sort of a context change. Also see
* {@link android.view.View#announceForAccessibility}
* @param view The view that triggered the announcement
* @param accessibilityManager AccessibilityManager instance. If it is null, the method can
* obtain an instance itself.
* @param text The announcement text
*/
public static void announceForAccessibility(
final View view, AccessibilityManager accessibilityManager,
final CharSequence text) {
// Jelly Bean added support for speaking text verbatim
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.announceForAccessibility(text);
return;
}
final Context context = view.getContext().getApplicationContext();
if (accessibilityManager == null) {
accessibilityManager = (AccessibilityManager) context.getSystemService(
Context.ACCESSIBILITY_SERVICE);
}
if (!accessibilityManager.isEnabled()) {
return;
}
final int eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED;
// Construct an accessibility event with the minimum recommended
// attributes. An event without a class name or package may be dropped.
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.getText().add(text);
event.setEnabled(view.isEnabled());
event.setClassName(view.getClass().getName());
event.setPackageName(context.getPackageName());
// JellyBean MR1 requires a source view to set the window ID.
final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
record.setSource(view);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
view.getParent().requestSendAccessibilityEvent(view, event);
} else {
// Sends the event directly through the accessibility manager.
accessibilityManager.sendAccessibilityEvent(event);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,68 @@
/*
* 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.
*/
package com.android.ex.photo.views;
import android.view.View;
import android.widget.ProgressBar;
/**
* This class wraps around two progress bars and is solely designed to fix
* a bug in the framework (b/6928449) that prevents a progress bar from
* gracefully switching back and forth between indeterminate and determinate
* modes.
*/
public class ProgressBarWrapper {
private final ProgressBar mDeterminate;
private final ProgressBar mIndeterminate;
private boolean mIsIndeterminate;
public ProgressBarWrapper(ProgressBar determinate,
ProgressBar indeterminate, boolean isIndeterminate) {
mDeterminate = determinate;
mIndeterminate = indeterminate;
setIndeterminate(isIndeterminate);
}
public void setIndeterminate(boolean isIndeterminate) {
mIsIndeterminate = isIndeterminate;
setVisibility(mIsIndeterminate);
}
public void setVisibility(int visibility) {
if (visibility == View.INVISIBLE || visibility == View.GONE) {
mIndeterminate.setVisibility(visibility);
mDeterminate.setVisibility(visibility);
} else {
setVisibility(mIsIndeterminate);
}
}
private void setVisibility(boolean isIndeterminate) {
mIndeterminate.setVisibility(isIndeterminate ? View.VISIBLE : View.GONE);
mDeterminate.setVisibility(isIndeterminate ? View.GONE : View.VISIBLE);
}
public void setMax(int max) {
mDeterminate.setMax(max);
}
public void setProgress(int progress) {
mDeterminate.setProgress(progress);
}
}