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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user