diff options
Diffstat (limited to 'src')
28 files changed, 1582 insertions, 596 deletions
diff --git a/src/com/android/camera/CameraSettings.java b/src/com/android/camera/CameraSettings.java index 31d31e128..64f3681de 100644 --- a/src/com/android/camera/CameraSettings.java +++ b/src/com/android/camera/CameraSettings.java @@ -235,8 +235,8 @@ public class CameraSettings { private void buildExposureCompensation( PreferenceGroup group, IconListPreference exposure) { - int max = mParameters.getMaxExposureCompensation(); - int min = mParameters.getMinExposureCompensation(); + int max = Math.min(3, mParameters.getMaxExposureCompensation()); + int min = Math.max(-3, mParameters.getMinExposureCompensation()); if (max == 0 && min == 0) { removePreference(group, exposure.getKey()); return; diff --git a/src/com/android/camera/Exif.java b/src/com/android/camera/Exif.java index 605556599..ee39d675e 100644 --- a/src/com/android/camera/Exif.java +++ b/src/com/android/camera/Exif.java @@ -18,9 +18,7 @@ package com.android.camera; import android.util.Log; -import com.android.gallery3d.exif.ExifInvalidFormatException; -import com.android.gallery3d.exif.ExifParser; -import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.exif.ExifInterface; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -31,44 +29,23 @@ public class Exif { // Returns the degrees in clockwise. Values are 0, 90, 180, or 270. public static int getOrientation(byte[] jpeg) { - if (jpeg == null) return 0; + if (jpeg == null) { + return 0; + } + ExifInterface exif = new ExifInterface(); InputStream is = new ByteArrayInputStream(jpeg); - try { - ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0); - int event = parser.next(); - while(event != ExifParser.EVENT_END) { - if (event == ExifParser.EVENT_NEW_TAG) { - ExifTag tag = parser.getTag(); - if (tag.getTagId() == ExifTag.TAG_ORIENTATION && - tag.hasValue()) { - int orient = (int) tag.getValueAt(0); - switch (orient) { - case ExifTag.Orientation.TOP_LEFT: - return 0; - case ExifTag.Orientation.BOTTOM_LEFT: - return 180; - case ExifTag.Orientation.RIGHT_TOP: - return 90; - case ExifTag.Orientation.RIGHT_BOTTOM: - return 270; - default: - Log.i(TAG, "Unsupported orientation"); - return 0; - } - } - } - event = parser.next(); + exif.readExif(is); + Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); + if (val == null) { + return 0; + } else { + return ExifInterface.getRotationForOrientationValue(val.shortValue()); } - Log.i(TAG, "Orientation not found"); - return 0; } catch (IOException e) { Log.w(TAG, "Failed to read EXIF orientation", e); return 0; - } catch (ExifInvalidFormatException e) { - Log.w(TAG, "Failed to read EXIF orientation", e); - return 0; } } } diff --git a/src/com/android/camera/PanoramaModule.java b/src/com/android/camera/PanoramaModule.java index 623d96dc6..dc27f56f1 100644 --- a/src/com/android/camera/PanoramaModule.java +++ b/src/com/android/camera/PanoramaModule.java @@ -60,10 +60,7 @@ import com.android.camera.ui.PopupManager; import com.android.camera.ui.Rotatable; import com.android.gallery3d.R; import com.android.gallery3d.common.ApiHelper; -import com.android.gallery3d.exif.ExifData; -import com.android.gallery3d.exif.ExifInvalidFormatException; -import com.android.gallery3d.exif.ExifOutputStream; -import com.android.gallery3d.exif.ExifReader; +import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; import com.android.gallery3d.ui.GLRootView; import com.android.gallery3d.util.UsageStatistics; @@ -913,31 +910,18 @@ public class PanoramaModule implements CameraModule, mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken); String filepath = Storage.generateFilepath(filename); - ExifOutputStream out = null; - InputStream is = null; + ExifInterface exif = new ExifInterface(); try { - is = new ByteArrayInputStream(jpegData); - ExifReader reader = new ExifReader(); - ExifData data = reader.read(is); - - // Add Exif tags. - data.addGpsDateTimeStampTag(mTimeTaken); - data.addDateTimeStampTag(ExifTag.TAG_DATE_TIME, mTimeTaken, TimeZone.getDefault()); - data.addTag(ExifTag.TAG_ORIENTATION). - setValue(getExifOrientation(orientation)); - - out = new ExifOutputStream(new FileOutputStream(filepath)); - out.setExifData(data); - out.write(jpegData); + exif.readExif(jpegData); + exif.addGpsDateTimeStampTag(mTimeTaken); + exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken, + TimeZone.getDefault()); + exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, + ExifInterface.getOrientationValueForRotation(orientation))); + exif.writeExif(jpegData, filepath); } catch (IOException e) { - Log.e(TAG, "Cannot set EXIF for " + filepath, e); + Log.e(TAG, "Cannot set exif for " + filepath, e); Storage.writeFile(filepath, jpegData); - } catch (ExifInvalidFormatException e) { - Log.e(TAG, "Cannot set EXIF for " + filepath, e); - Storage.writeFile(filepath, jpegData); - } finally { - Util.closeSilently(out); - Util.closeSilently(is); } int jpegLength = (int) (new File(filepath).length()); @@ -947,21 +931,6 @@ public class PanoramaModule implements CameraModule, return null; } - private static int getExifOrientation(int orientation) { - switch (orientation) { - case 0: - return ExifTag.Orientation.TOP_LEFT; - case 90: - return ExifTag.Orientation.RIGHT_TOP; - case 180: - return ExifTag.Orientation.BOTTOM_LEFT; - case 270: - return ExifTag.Orientation.RIGHT_BOTTOM; - default: - throw new AssertionError("invalid: " + orientation); - } - } - private void clearMosaicFrameProcessorIfNeeded() { if (!mPaused || mThreadRunning) return; // Only clear the processor if it is initialized by this activity diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java index 995996480..18d0d3e86 100644 --- a/src/com/android/camera/PhotoUI.java +++ b/src/com/android/camera/PhotoUI.java @@ -676,7 +676,8 @@ public class PhotoUI implements PieListener, @Override public void clearFocus() { - getFocusIndicator().clear(); + FocusIndicator indicator = getFocusIndicator(); + if (indicator != null) indicator.clear(); } @Override diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java new file mode 100644 index 000000000..5d10953f8 --- /dev/null +++ b/src/com/android/camera/data/CameraDataAdapter.java @@ -0,0 +1,330 @@ +/* + * 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.camera.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.drawable.ColorDrawable; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; + +import com.android.camera.Storage; +import com.android.camera.ui.FilmStripView; + +import java.util.ArrayList; +import java.util.List; + +/** + * A FilmStripDataProvider that provide data in the camera folder. + * + * The given view for camera preview won't be added until the preview info + * has been set by setPreviewInfo(int, int, int) + */ +public class CameraDataAdapter implements FilmStripView.DataAdapter { + private static final String TAG = "CamreaFilmStripDataProvider"; + + private static final int DEFAULT_DECODE_SIZE = 3000; + private static final String ORDER_CLAUSE = ImageColumns.DATE_TAKEN + " DESC, " + + ImageColumns._ID + " DESC"; + private static final String[] CAMERA_PATH = { Storage.DIRECTORY + "%" }; + private static final int COL_ID = 0; + private static final int COL_TITLE = 1; + private static final int COL_MIME_TYPE = 2; + private static final int COL_DATE_TAKEN = 3; + private static final int COL_DATE_MODIFIED = 4; + private static final int COL_DATA = 5; + private static final int COL_ORIENTATION = 6; + private static final int COL_WIDTH = 7; + private static final int COL_HEIGHT = 8; + private static final int COL_SIZE = 9; + + private static final String[] PROJECTION = { + ImageColumns._ID, // 0, int + ImageColumns.TITLE, // 1, string + ImageColumns.MIME_TYPE, // 2, tring + ImageColumns.DATE_TAKEN, // 3, int + ImageColumns.DATE_MODIFIED, // 4, int + ImageColumns.DATA, // 5, string + ImageColumns.ORIENTATION, // 6, int, 0, 90, 180, 270 + ImageColumns.WIDTH, // 7, int + ImageColumns.HEIGHT, // 8, int + ImageColumns.SIZE // 9, int + }; + + // 32K buffer. + private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024]; + + private List<LocalImageData> mImages; + + private FilmStripView mFilmStripView; + private View mCameraPreviewView; + private ColorDrawable mPlaceHolder; + + private int mSuggestedWidth = DEFAULT_DECODE_SIZE; + private int mSuggestedHeight = DEFAULT_DECODE_SIZE; + + public CameraDataAdapter(View cameraPreviewView, int placeHolderColor) { + mCameraPreviewView = cameraPreviewView; + mPlaceHolder = new ColorDrawable(placeHolderColor); + } + + public void setCameraPreviewInfo(int width, int height, int orientation) { + addOrReplaceCameraData(buildCameraImageData(width, height, orientation)); + } + + @Override + public int getTotalNumber() { + return mImages.size(); + } + + @Override + public FilmStripView.ImageData getImageData(int id) { + if (id >= mImages.size()) return null; + return mImages.get(id); + } + + @Override + public void suggestSize(int w, int h) { + if (w <= 0 || h <= 0) { + mSuggestedWidth = mSuggestedHeight = DEFAULT_DECODE_SIZE; + } else { + mSuggestedWidth = (w < DEFAULT_DECODE_SIZE ? w : DEFAULT_DECODE_SIZE); + mSuggestedHeight = (h < DEFAULT_DECODE_SIZE ? h : DEFAULT_DECODE_SIZE); + } + } + + @Override + public void requestLoad(ContentResolver resolver) { + QueryTask qtask = new QueryTask(); + qtask.execute(resolver); + } + + @Override + public View getView(Context c, int dataID) { + if (dataID >= mImages.size() || dataID < 0) { + return null; + } + + LocalImageData data = mImages.get(dataID); + + if (data.isCameraData) return mCameraPreviewView; + + ImageView v = new ImageView(c); + v.setImageDrawable(mPlaceHolder); + + v.setScaleType(ImageView.ScaleType.FIT_XY); + LoadBitmapTask task = new LoadBitmapTask(data, v); + task.execute(); + return v; + } + + @Override + public void setDataListener(FilmStripView v) { + mFilmStripView = v; + } + + private LocalImageData buildCameraImageData(int width, int height, int orientation) { + LocalImageData d = new LocalImageData(); + d.width = width; + d.height = height; + d.orientation = orientation; + d.isCameraData = true; + return d; + } + + private void addOrReplaceCameraData(LocalImageData data) { + if (mImages == null) mImages = new ArrayList<LocalImageData>(); + if (mImages.size() == 0) { + mImages.add(0, data); + return; + } + + LocalImageData first = mImages.get(0); + if (first.isCameraData) { + mImages.set(0, data); + } else { + mImages.add(0, data); + } + } + + private LocalImageData buildCursorImageData(Cursor c) { + LocalImageData d = new LocalImageData(); + d.id = c.getInt(COL_ID); + d.title = c.getString(COL_TITLE); + d.mimeType = c.getString(COL_MIME_TYPE); + d.path = c.getString(COL_DATA); + d.orientation = c.getInt(COL_ORIENTATION); + d.width = c.getInt(COL_WIDTH); + d.height = c.getInt(COL_HEIGHT); + if (d.width <= 0 || d.height <= 0) { + Log.v(TAG, "warning! zero dimension for " + + d.path + ":" + d.width + "x" + d.height); + Dimension dim = decodeDimension(d.path); + if (dim != null) { + d.width = dim.width; + d.height = dim.height; + } else { + Log.v(TAG, "warning! dimension decode failed for " + d.path); + Bitmap b = BitmapFactory.decodeFile(d.path); + if (b == null) return null; + d.width = b.getWidth(); + d.height = b.getHeight(); + } + } + if (d.orientation == 90 || d.orientation == 270) { + int b = d.width; + d.width = d.height; + d.height = b; + } + return d; + } + + private Dimension decodeDimension(String path) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + Bitmap b = BitmapFactory.decodeFile(path, opts); + if (b == null) return null; + Dimension d = new Dimension(); + d.width = opts.outWidth; + d.height = opts.outHeight; + return d; + } + + private class Dimension { + public int width; + public int height; + } + + private class LocalImageData implements FilmStripView.ImageData { + public boolean isCameraData; + public int id; + public String title; + public String mimeType; + public String path; + // from MediaStore, can only be 0, 90, 180, 270; + public int orientation; + // width and height should be adjusted according to orientation. + public int width; + public int height; + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public String toString() { + return "LocalImageData:" + ",data=" + path + ",mimeType=" + mimeType + + "," + width + "x" + height + ",orientation=" + orientation; + } + } + + private class QueryTask extends AsyncTask<ContentResolver, Void, List<LocalImageData>> { + private ContentResolver mResolver; + private LocalImageData mCameraImageData; + + @Override + protected List<LocalImageData> doInBackground(ContentResolver... resolver) { + List<LocalImageData> l = null; + Cursor c = resolver[0].query(Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, + MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH, + ORDER_CLAUSE); + if (c == null) return null; + l = new ArrayList<LocalImageData>(); + c.moveToFirst(); + while (!c.isLast()) { + LocalImageData data = buildCursorImageData(c); + if (data != null) l.add(data); + else Log.e(TAG, "Error decoding file:" + c.getString(COL_DATA)); + c.moveToNext(); + } + c.close(); + return l; + } + + @Override + protected void onPostExecute(List<LocalImageData> l) { + boolean changed = (l != mImages); + LocalImageData first = null; + if (mImages != null && mImages.size() > 0) { + first = mImages.get(0); + if (!first.isCameraData) first = null; + } + mImages = l; + if (first != null) addOrReplaceCameraData(first); + // both might be null. + if (changed) mFilmStripView.onDataChanged(); + } + } + + private class LoadBitmapTask extends AsyncTask<Void, Void, Bitmap> { + private LocalImageData mData; + private ImageView mView; + + public LoadBitmapTask( + LocalImageData d, ImageView v) { + mData = d; + mView = v; + } + + @Override + protected Bitmap doInBackground(Void... v) { + BitmapFactory.Options opts = null; + Bitmap b; + int sample = 1; + while (mSuggestedWidth * sample < mData.width + || mSuggestedHeight * sample < mData.height) { + sample *= 2; + } + opts = new BitmapFactory.Options(); + opts.inSampleSize = sample; + opts.inTempStorage = DECODE_TEMP_STORAGE; + if (isCancelled()) return null; + b = BitmapFactory.decodeFile(mData.path, opts); + if (mData.orientation != 0) { + if (isCancelled()) return null; + Matrix m = new Matrix(); + m.setRotate((float) mData.orientation); + b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false); + } + return b; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap == null) { + Log.e(TAG, "Cannot decode bitmap file:" + mData.path); + return; + } + mView.setImageBitmap(bitmap); + } + } +} diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java index 75906acbe..e9551ada2 100644 --- a/src/com/android/camera/ui/CameraSwitcher.java +++ b/src/com/android/camera/ui/CameraSwitcher.java @@ -107,6 +107,7 @@ public class CameraSwitcher extends RotateImageView private void onCameraSelected(int ix) { hidePopup(); if ((ix != mCurrentIndex) && (mListener != null)) { + UsageStatistics.onEvent("CameraModeSwitch", null, null); UsageStatistics.setPendingTransitionCause( UsageStatistics.TRANSITION_MENU_TAP); setCurrentIndex(ix); diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java new file mode 100644 index 000000000..326e969b2 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripView.java @@ -0,0 +1,421 @@ +/* + * 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.camera.ui; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Scroller; + +public class FilmStripView extends ViewGroup { + + private static final String TAG = "FilmStripView"; + private static final int BUFFER_SIZE = 5; + // Horizontal padding of children. + private static final int H_PADDING = 50; + // Duration to go back to the first. + private static final int BACK_SCROLL_DURATION = 500; + private static final float MIN_SCALE = 0.7f; + + private Context mContext; + private GestureDetector mGestureDetector; + private DataAdapter mDataAdapter; + private final Rect mDrawArea = new Rect(); + + private int mCurrentInfo; + private Scroller mScroller; + private boolean mIsScrolling; + private int mCenterPosition = -1; + private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE]; + + public interface ImageData { + // The values returned by getWidth() and getHeight() will be used for layout. + public int getWidth(); + public int getHeight(); + } + + public interface DataAdapter { + + public int getTotalNumber(); + public View getView(Context context, int id); + public ImageData getImageData(int id); + public void suggestSize(int w, int h); + + public void requestLoad(ContentResolver r); + public void setDataListener(FilmStripView v); + } + + private static class ViewInfo { + private int mDataID; + // the position of the left of the view in the whole filmstrip. + private int mLeftPosition; + private View mView; + + public ViewInfo(int id, View v) { + mDataID = id; + mView = v; + mLeftPosition = -1; + } + + public int getId() { + return mDataID; + } + + public void setLeftPosition(int pos) { + mLeftPosition = pos; + } + + public int getLeftPosition() { + return mLeftPosition; + } + + public int getCenterPosition() { + return mLeftPosition + mView.getWidth() / 2; + } + + public View getView() { + return mView; + } + + private void layoutAt(int l, int t) { + mView.layout(l, t, l + mView.getMeasuredWidth(), t + mView.getMeasuredHeight()); + } + + public void layoutIn(Rect drawArea, int refCenter) { + // drawArea is where to layout in. + // refCenter is the absolute horizontal position of the center of drawArea. + layoutAt(drawArea.centerX() + mLeftPosition - refCenter, + drawArea.centerY() - mView.getMeasuredHeight() / 2); + } + } + + public FilmStripView(Context context) { + super(context); + init(context); + } + + public FilmStripView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public FilmStripView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + mCurrentInfo = (BUFFER_SIZE - 1) / 2; + setWillNotDraw(false); + mContext = context; + mScroller = new Scroller(context); + mGestureDetector = + new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + } + + @Override + public void onDraw(Canvas c) { + if (mIsScrolling) { + layoutChildren(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int w = MeasureSpec.getSize(widthMeasureSpec); + int h = MeasureSpec.getSize(heightMeasureSpec); + float scale = MIN_SCALE; + if (mDataAdapter != null) mDataAdapter.suggestSize(w / 2, h / 2); + + int boundWidth = (int) (w * scale); + int boundHeight = (int) (h * scale); + + int wMode = View.MeasureSpec.EXACTLY; + int hMode = View.MeasureSpec.EXACTLY; + + for (int i = 0; i < mViewInfo.length; i++) { + ViewInfo info = mViewInfo[i]; + if (mViewInfo[i] == null) continue; + + int imageWidth = mDataAdapter.getImageData(info.getId()).getWidth(); + int imageHeight = mDataAdapter.getImageData(info.getId()).getHeight(); + + int scaledWidth = boundWidth; + int scaledHeight = boundHeight; + if (imageWidth * scaledHeight > scaledWidth * imageHeight) { + scaledHeight = imageHeight * scaledWidth / imageWidth; + } else { + scaledWidth = imageWidth * scaledHeight / imageHeight; + } + scaledWidth += H_PADDING * 2 * scale; + mViewInfo[i].getView().measure( + View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode) + , View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode)); + } + setMeasuredDimension(w, h); + } + + private int findTheNearestView(int pointX) { + + int nearest = 0; + // find the first non-null ViewInfo. + for (; nearest < BUFFER_SIZE + && (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1); + nearest++); + // no existing available ViewInfo + if (nearest == BUFFER_SIZE) return -1; + int min = Math.abs(pointX - mViewInfo[nearest].getCenterPosition()); + + for (int infoID = nearest + 1; + infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) { + // not measured yet. + if (mViewInfo[infoID].getLeftPosition() == -1) continue; + + int c = mViewInfo[infoID].getCenterPosition(); + int dist = Math.abs(pointX - c); + if (dist < min) { + min = dist; + nearest = infoID; + } + } + return nearest; + } + + // We try to keep the one closest to the center of the screen at position mCurrentInfo. + private void stepIfNeeded() { + int nearest = findTheNearestView(mCenterPosition); + // no change made. + if (nearest == -1 || nearest == mCurrentInfo) return; + + int adjust = nearest - mCurrentInfo; + if (adjust > 0) { + for (int k = 0; k < adjust; k++) { + if (mViewInfo[k] != null) { + removeView(mViewInfo[k].getView()); + } + } + for (int k = 0; k + adjust < BUFFER_SIZE; k++) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) { + mViewInfo[k] = null; + if (mViewInfo[k - 1] != null) getInfo(k, mViewInfo[k - 1].getId() + 1); + } + } else { + for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { + if (mViewInfo[k] != null) { + removeView(mViewInfo[k].getView()); + } + } + for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = -1 - adjust; k >= 0; k--) { + mViewInfo[k] = null; + if (mViewInfo[k + 1] != null) getInfo(k, mViewInfo[k + 1].getId() - 1); + } + } + } + + private void stopScroll() { + mScroller.forceFinished(true); + mIsScrolling = false; + } + + private void adjustCenterPosition() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) return; + + if (curr.getId() == 0 && mCenterPosition < curr.getCenterPosition()) { + mCenterPosition = curr.getCenterPosition(); + if (mIsScrolling) stopScroll(); + } + if (curr.getId() == mDataAdapter.getTotalNumber() - 1 + && mCenterPosition > curr.getCenterPosition()) { + mCenterPosition = curr.getCenterPosition(); + if (mIsScrolling) stopScroll(); + } + } + + private void layoutChildren() { + mIsScrolling = mScroller.computeScrollOffset(); + + if (mIsScrolling) mCenterPosition = mScroller.getCurrX(); + + adjustCenterPosition(); + + mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition); + + // images on the left + for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) { + ViewInfo curr = mViewInfo[infoID]; + if (curr != null) { + ViewInfo next = mViewInfo[infoID + 1]; + curr.setLeftPosition(next.getLeftPosition() - curr.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition); + } + } + + // images on the right + for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) { + ViewInfo curr = mViewInfo[infoID]; + if (curr != null) { + ViewInfo prev = mViewInfo[infoID - 1]; + curr.setLeftPosition(prev.getLeftPosition() + prev.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition); + } + } + + stepIfNeeded(); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mViewInfo[mCurrentInfo] == null) return; + + mDrawArea.left = l; + mDrawArea.top = t; + mDrawArea.right = r; + mDrawArea.bottom = b; + + layoutChildren(); + } + + public void setDataAdapter( + DataAdapter adapter, ContentResolver resolver) { + mDataAdapter = adapter; + mDataAdapter.suggestSize(getMeasuredWidth(), getMeasuredHeight()); + mDataAdapter.setDataListener(this); + mDataAdapter.requestLoad(resolver); + } + + private void getInfo(int infoID, int dataID) { + View v = mDataAdapter.getView(mContext, dataID); + if (v == null) return; + v.setPadding(H_PADDING, 0, H_PADDING, 0); + addView(v); + ViewInfo info = new ViewInfo(dataID, v); + mViewInfo[infoID] = info; + } + + public void onDataChanged() { + removeAllViews(); + int dataNumber = mDataAdapter.getTotalNumber(); + if (dataNumber == 0) return; + + int currentData = 0; + int currentLeft = 0; + // previous data exists. + if (mViewInfo[mCurrentInfo] != null) { + currentLeft = mViewInfo[mCurrentInfo].getLeftPosition(); + currentData = mViewInfo[mCurrentInfo].getId(); + } + getInfo(mCurrentInfo, currentData); + mViewInfo[mCurrentInfo].setLeftPosition(currentLeft); + for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) { + int infoID = mCurrentInfo + i; + if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) { + getInfo(infoID, mViewInfo[infoID - 1].getId() + 1); + } + infoID = mCurrentInfo - i; + if (infoID >= 0 && mViewInfo[infoID + 1] != null) { + getInfo(infoID, mViewInfo[infoID + 1].getId() - 1); + } + } + layoutChildren(); + } + + private void movePositionTo(int position) { + mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition, + 0, BACK_SCROLL_DURATION); + layoutChildren(); + } + + public void goToFirst() { + movePositionTo(0); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mGestureDetector.onTouchEvent(ev); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onDoubleTap(MotionEvent e) { + float x = (float) e.getX(); + float y = (float) e.getY(); + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) continue; + View v = mViewInfo[i].getView(); + if (x >= v.getLeft() && x < v.getRight() + && y >= v.getTop() && y < v.getBottom()) { + Log.v(TAG, "l, r, t, b " + v.getLeft() + ',' + v.getRight() + + ',' + v.getTop() + ',' + v.getBottom()); + movePositionTo(mViewInfo[i].getCenterPosition()); + break; + } + } + return true; + } + + @Override + public boolean onDown(MotionEvent ev) { + if (mIsScrolling) stopScroll(); + return true; + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + stopScroll(); + mCenterPosition += dx; + layoutChildren(); + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + ViewInfo info = mViewInfo[mCurrentInfo]; + int w = getWidth(); + if (info == null) return true; + mScroller.fling(mCenterPosition, 0, (int) -velocityX, (int) velocityY, + // estimation of possible length on the left + info.getLeftPosition() - info.getId() * w * 2, + // estimation of possible length on the right + info.getLeftPosition() + + (mDataAdapter.getTotalNumber() - info.getId()) * w * 2, + 0, 0); + layoutChildren(); + return true; + } + } +} diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java index 84030896a..fe6840ecb 100644 --- a/src/com/android/gallery3d/app/PhotoPage.java +++ b/src/com/android/gallery3d/app/PhotoPage.java @@ -514,6 +514,10 @@ public abstract class PhotoPage extends ActivityState implements if (oldIndex == 0 && mCurrentIndex > 0 && !mPhotoView.getFilmMode()) { mPhotoView.setFilmMode(true); + if (mAppBridge != null) { + UsageStatistics.onEvent("CameraToFilmstrip", + UsageStatistics.TRANSITION_SWIPE, null); + } } else if (oldIndex == 2 && mCurrentIndex == 1) { mCameraSwitchCutoff = SystemClock.uptimeMillis() + CAMERA_SWITCH_CUTOFF_THRESHOLD_MS; diff --git a/src/com/android/gallery3d/data/Exif.java b/src/com/android/gallery3d/data/Exif.java index 30aba7e97..950e7de18 100644 --- a/src/com/android/gallery3d/data/Exif.java +++ b/src/com/android/gallery3d/data/Exif.java @@ -18,55 +18,31 @@ package com.android.gallery3d.data; import android.util.Log; -import com.android.gallery3d.exif.ExifInvalidFormatException; -import com.android.gallery3d.exif.ExifParser; -import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.exif.ExifInterface; import java.io.IOException; import java.io.InputStream; public class Exif { - private static final String TAG = "GalleryExif"; + private static final String TAG = "CameraExif"; + // Returns the degrees in clockwise. Values are 0, 90, 180, or 270. public static int getOrientation(InputStream is) { if (is == null) { return 0; } - + ExifInterface exif = new ExifInterface(); try { - ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0); - int event = parser.next(); - while (event != ExifParser.EVENT_END) { - if (event == ExifParser.EVENT_NEW_TAG) { - ExifTag tag = parser.getTag(); - if (tag.getTagId() == ExifTag.TAG_ORIENTATION && - tag.hasValue()) { - int orient = (int) tag.getValueAt(0); - switch (orient) { - case ExifTag.Orientation.TOP_LEFT: - return 0; - case ExifTag.Orientation.BOTTOM_LEFT: - return 180; - case ExifTag.Orientation.RIGHT_TOP: - return 90; - case ExifTag.Orientation.RIGHT_BOTTOM: - return 270; - default: - Log.i(TAG, "Unsupported orientation"); - return 0; - } - } - } - event = parser.next(); + exif.readExif(is); + Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); + if (val == null) { + return 0; + } else { + return ExifInterface.getRotationForOrientationValue(val.shortValue()); } - Log.i(TAG, "Orientation not found"); - return 0; } catch (IOException e) { Log.w(TAG, "Failed to read EXIF orientation", e); return 0; - } catch (ExifInvalidFormatException e) { - Log.w(TAG, "Failed to read EXIF orientation", e); - return 0; } } } diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java index d5fad5483..1ed67ecf4 100644 --- a/src/com/android/gallery3d/data/LocalImage.java +++ b/src/com/android/gallery3d/data/LocalImage.java @@ -23,7 +23,6 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; -import android.media.ExifInterface; import android.net.Uri; import android.os.Build; import android.provider.MediaStore.Images; @@ -37,8 +36,7 @@ import com.android.gallery3d.app.StitchingProgressManager; import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.common.BitmapUtils; import com.android.gallery3d.common.Utils; -import com.android.gallery3d.exif.ExifInvalidFormatException; -import com.android.gallery3d.exif.ExifModifier; +import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; import com.android.gallery3d.util.GalleryUtils; import com.android.gallery3d.util.ThreadPool.Job; @@ -46,6 +44,7 @@ import com.android.gallery3d.util.ThreadPool.JobContext; import com.android.gallery3d.util.UpdateHelper; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileChannel.MapMode; @@ -196,15 +195,15 @@ public class LocalImage extends LocalMediaItem { // try to decode from JPEG EXIF if (type == MediaItem.TYPE_MICROTHUMBNAIL) { - ExifInterface exif = null; - byte [] thumbData = null; + ExifInterface exif = new ExifInterface(); + byte[] thumbData = null; try { - exif = new ExifInterface(mLocalFilePath); - if (exif != null) { - thumbData = exif.getThumbnail(); - } - } catch (Throwable t) { - Log.w(TAG, "fail to get exif thumb", t); + exif.readExif(mLocalFilePath); + thumbData = exif.getThumbnail(); + } catch (FileNotFoundException e) { + Log.w(TAG, "failed to find file to read thumbnail: " + mLocalFilePath); + } catch (IOException e) { + Log.w(TAG, "failed to get thumbnail from: " + mLocalFilePath); } if (thumbData != null) { Bitmap bitmap = DecodeUtils.decodeIfBigEnough( @@ -276,21 +275,6 @@ public class LocalImage extends LocalMediaItem { new String[]{String.valueOf(id)}); } - private static int getExifOrientation(int orientation) { - switch (orientation) { - case 0: - return ExifInterface.ORIENTATION_NORMAL; - case 90: - return ExifInterface.ORIENTATION_ROTATE_90; - case 180: - return ExifInterface.ORIENTATION_ROTATE_180; - case 270: - return ExifInterface.ORIENTATION_ROTATE_270; - default: - throw new AssertionError("invalid: " + orientation); - } - } - @Override public void rotate(int degrees) { GalleryUtils.assertNotInRenderThread(); @@ -300,34 +284,22 @@ public class LocalImage extends LocalMediaItem { if (rotation < 0) rotation += 360; if (mimeType.equalsIgnoreCase("image/jpeg")) { - RandomAccessFile file = null; - try { - // Because most of the images contain the orientation tag, we - // use ExifModifier to modify the tag for better efficiency. - // If the tag doesn't exist, ExifInterface will be used to replace the entire - // header. - file = new RandomAccessFile(filePath, "rw"); - ExifModifier modifier = new ExifModifier( - file.getChannel().map(MapMode.READ_WRITE, 0, file.length())); - ExifTag tag = ExifTag.buildTag(ExifTag.TAG_ORIENTATION); - tag.setValue(getExifOrientation(rotation)); - modifier.modifyTag(tag); - if (!modifier.commit()) { - // Need to change the file size, use ExifInterface instead. - ExifInterface exif = new ExifInterface(filePath); - exif.setAttribute(ExifInterface.TAG_ORIENTATION, - String.valueOf(getExifOrientation(rotation))); - exif.saveAttributes(); - // We need to update the filesize as well + ExifInterface exifInterface = new ExifInterface(); + ExifTag tag = exifInterface.buildTag(ExifInterface.TAG_ORIENTATION, + ExifInterface.getOrientationValueForRotation(rotation)); + if(tag != null) { + exifInterface.setTag(tag); + try { + exifInterface.forceRewriteExif(filePath); fileSize = new File(filePath).length(); values.put(Images.Media.SIZE, fileSize); + } catch (FileNotFoundException e) { + Log.w(TAG, "cannot find file to set exif: " + filePath); + } catch (IOException e) { + Log.w(TAG, "cannot set exif data: " + filePath); } - } catch (IOException e) { - Log.w(TAG, "cannot set exif data: " + filePath); - } catch (ExifInvalidFormatException e) { - Log.w(TAG, "cannot set exif data: " + filePath); - } finally { - Utils.closeSilently(file); + } else { + Log.w(TAG, "Could not build tag: " + ExifInterface.TAG_ORIENTATION); } } diff --git a/src/com/android/gallery3d/data/MediaDetails.java b/src/com/android/gallery3d/data/MediaDetails.java index 662bd141c..cac524b88 100644 --- a/src/com/android/gallery3d/data/MediaDetails.java +++ b/src/com/android/gallery3d/data/MediaDetails.java @@ -18,12 +18,12 @@ package com.android.gallery3d.data; import com.android.gallery3d.R; import com.android.gallery3d.common.Utils; -import com.android.gallery3d.exif.ExifData; -import com.android.gallery3d.exif.ExifInvalidFormatException; -import com.android.gallery3d.exif.ExifReader; +import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.exif.Rational; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; @@ -115,11 +115,11 @@ public class MediaDetails implements Iterable<Entry<Integer, Object>> { String value = null; int type = tag.getDataType(); if (type == ExifTag.TYPE_UNSIGNED_RATIONAL || type == ExifTag.TYPE_RATIONAL) { - value = String.valueOf(tag.getRational(0).toDouble()); + value = String.valueOf(tag.getValueAsRational(0).toDouble()); } else if (type == ExifTag.TYPE_ASCII) { - value = tag.getString(); + value = tag.getValueAsString(); } else { - value = String.valueOf(tag.getValueAt(0)); + value = String.valueOf(tag.forceGetValueAsLong(0)); } if (key == MediaDetails.INDEX_FLASH) { MediaDetails.FlashState state = new MediaDetails.FlashState( @@ -132,37 +132,39 @@ public class MediaDetails implements Iterable<Entry<Integer, Object>> { } public static void extractExifInfo(MediaDetails details, String filePath) { - InputStream is = null; + + ExifInterface exif = new ExifInterface(); try { - is = new FileInputStream(filePath); - ExifData data = new ExifReader().read(is); - setExifData(details, data.getTag(ExifTag.TAG_FLASH), MediaDetails.INDEX_FLASH); - setExifData(details, data.getTag(ExifTag.TAG_IMAGE_WIDTH), MediaDetails.INDEX_WIDTH); - setExifData(details, data.getTag(ExifTag.TAG_IMAGE_LENGTH), MediaDetails.INDEX_HEIGHT); - setExifData(details, data.getTag(ExifTag.TAG_MAKE), MediaDetails.INDEX_MAKE); - setExifData(details, data.getTag(ExifTag.TAG_MODEL),MediaDetails.INDEX_MODEL); - setExifData(details, data.getTag(ExifTag.TAG_APERTURE_VALUE), - MediaDetails.INDEX_APERTURE); - setExifData(details, data.getTag(ExifTag.TAG_ISO_SPEED_RATINGS), - MediaDetails.INDEX_ISO); - setExifData(details, data.getTag(ExifTag.TAG_WHITE_BALANCE), - MediaDetails.INDEX_WHITE_BALANCE); - setExifData(details, data.getTag(ExifTag.TAG_EXPOSURE_TIME), - MediaDetails.INDEX_EXPOSURE_TIME); - ExifTag focalTag = data.getTag(ExifTag.TAG_FOCAL_LENGTH); - if (focalTag != null) { - details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH, - focalTag.getRational(0).toDouble()); - details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm); - } - } catch (IOException ex) { - // ignore it. - Log.w(TAG, "", ex); - } catch (ExifInvalidFormatException ex) { - // ignore it. - Log.w(TAG, "", ex); - } finally { - Utils.closeSilently(is); + exif.readExif(filePath); + } catch (FileNotFoundException e) { + Log.w(TAG, "Could not find file to read exif: " + filePath, e); + } catch (IOException e) { + Log.w(TAG, "Could not read exif from file: " + filePath, e); + } + + setExifData(details, exif.getTag(ExifInterface.TAG_FLASH), + MediaDetails.INDEX_FLASH); + setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_WIDTH), + MediaDetails.INDEX_WIDTH); + setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_LENGTH), + MediaDetails.INDEX_HEIGHT); + setExifData(details, exif.getTag(ExifInterface.TAG_MAKE), + MediaDetails.INDEX_MAKE); + setExifData(details, exif.getTag(ExifInterface.TAG_MODEL), + MediaDetails.INDEX_MODEL); + setExifData(details, exif.getTag(ExifInterface.TAG_APERTURE_VALUE), + MediaDetails.INDEX_APERTURE); + setExifData(details, exif.getTag(ExifInterface.TAG_ISO_SPEED_RATINGS), + MediaDetails.INDEX_ISO); + setExifData(details, exif.getTag(ExifInterface.TAG_WHITE_BALANCE), + MediaDetails.INDEX_WHITE_BALANCE); + setExifData(details, exif.getTag(ExifInterface.TAG_EXPOSURE_TIME), + MediaDetails.INDEX_EXPOSURE_TIME); + ExifTag focalTag = exif.getTag(ExifInterface.TAG_FOCAL_LENGTH); + if (focalTag != null) { + details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH, + focalTag.getValueAsRational(0).toDouble()); + details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm); } } } diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java index 93bb02483..37b2cd9da 100644 --- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java +++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java @@ -574,6 +574,7 @@ public class FilterShowActivity extends Activity implements OnItemClickListener, // TODO: Using singletons is a bad design choice for many of these // due static reference leaks and in general. Please refactor. MasterImage.reset(); + ImageFilterRS.destroyRenderScriptContext(); FilteringPipeline.reset(); ImageFilter.resetStatics(); FiltersManager.reset(); diff --git a/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java b/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java index 4aeb580e6..d7e9a62a7 100644 --- a/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java +++ b/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java @@ -30,7 +30,7 @@ import com.android.gallery3d.filtershow.presets.ImagePreset; public class FilteringPipeline implements Handler.Callback { - private static FilteringPipeline sPipeline; + private static volatile FilteringPipeline sPipeline = null; private static final String LOGTAG = "FilteringPipeline"; private ImagePreset mPreviousGeometryPreset = null; private ImagePreset mPreviousFiltersPreset = null; @@ -120,7 +120,7 @@ public class FilteringPipeline implements Handler.Callback { mProcessingHandler = new Handler(mHandlerThread.getLooper(), this); } - public static FilteringPipeline getPipeline() { + public synchronized static FilteringPipeline getPipeline() { if (sPipeline == null) { sPipeline = new FilteringPipeline(); } @@ -173,8 +173,6 @@ public class FilteringPipeline implements Handler.Callback { Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); mPreviousGeometry = new GeometryMetadata(geometry); - - FiltersManager.getManager().resetBitmapsRS(); return true; } @@ -259,17 +257,19 @@ public class FilteringPipeline implements Handler.Callback { preset.setupEnvironment(); if (request.getType() == RenderingRequest.PARTIAL_RENDERING) { - bitmap = MasterImage.getImage().getImageLoader().getScaleOneImageForPreset(null, preset, + ImageLoader loader = MasterImage.getImage().getImageLoader(); + if (loader == null) { + Log.w(LOGTAG, "loader not yet setup, cannot handle: " + getType(request)); + return; + } + bitmap = loader.getScaleOneImageForPreset(null, preset, request.getBounds(), request.getDestination(), false); if (bitmap == null) { + Log.w(LOGTAG, "could not get bitmap for: " + getType(request)); return; } } - if (request.getType() == RenderingRequest.FILTERS_RENDERING) { - FiltersManager.getManager().resetBitmapsRS(); - } - if (request.getType() != RenderingRequest.ICON_RENDERING && request.getType() != RenderingRequest.PARTIAL_RENDERING) { updateOriginalAllocation(preset); @@ -296,9 +296,6 @@ public class FilteringPipeline implements Handler.Callback { FiltersManager.getManager().freeFilterResources(preset); } - if (request.getType() == RenderingRequest.FILTERS_RENDERING) { - FiltersManager.getManager().resetBitmapsRS(); - } } private void compute(TripleBufferBitmap buffer, ImagePreset preset, int type) { @@ -356,7 +353,8 @@ public class FilteringPipeline implements Handler.Callback { return mPreviewScaleFactor; } - public static void reset() { + public static synchronized void reset() { + sPipeline.mHandlerThread.quit(); sPipeline = null; } } diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java index 6cf462269..2c1a847f8 100644 --- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java +++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java @@ -27,7 +27,7 @@ import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Matrix; import android.graphics.Rect; -import android.media.ExifInterface; +import android.graphics.Bitmap.CompressFormat; import android.net.Uri; import android.provider.MediaStore; import android.util.Log; @@ -36,9 +36,8 @@ import com.adobe.xmp.XMPException; import com.adobe.xmp.XMPMeta; import com.android.gallery3d.R; import com.android.gallery3d.common.Utils; -import com.android.gallery3d.exif.ExifInvalidFormatException; -import com.android.gallery3d.exif.ExifParser; import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.filtershow.FilterShowActivity; import com.android.gallery3d.filtershow.HistoryAdapter; import com.android.gallery3d.filtershow.imageshow.ImageShow; @@ -48,6 +47,8 @@ import com.android.gallery3d.filtershow.tools.SaveCopyTask; import com.android.gallery3d.util.InterruptableOutputStream; import com.android.gallery3d.util.XmpUtilHelper; +import java.io.ByteArrayInputStream; +import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -79,14 +80,14 @@ public class ImageLoader { public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; public static final int DEFAULT_COMPRESS_QUALITY = 95; - public static final int ORI_NORMAL = ExifInterface.ORIENTATION_NORMAL; - public static final int ORI_ROTATE_90 = ExifInterface.ORIENTATION_ROTATE_90; - public static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180; - public static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270; - public static final int ORI_FLIP_HOR = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; - public static final int ORI_FLIP_VERT = ExifInterface.ORIENTATION_FLIP_VERTICAL; - public static final int ORI_TRANSPOSE = ExifInterface.ORIENTATION_TRANSPOSE; - public static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE; + public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT; + public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP; + public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT; + public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM; + public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT; + public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT; + public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP; + public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM; private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5; private Context mContext = null; @@ -147,26 +148,13 @@ public class ImageLoader { String path = uri.getPath(); int orientation = -1; InputStream is = null; + ExifInterface exif = new ExifInterface(); try { - is = new FileInputStream(path); - ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0); - int event = parser.next(); - while (event != ExifParser.EVENT_END) { - if (event == ExifParser.EVENT_NEW_TAG) { - ExifTag tag = parser.getTag(); - if (tag.getTagId() == ExifTag.TAG_ORIENTATION) { - orientation = (int) tag.getValueAt(0); - break; - } - } - event = parser.next(); - } + exif.readExif(path); + orientation = ExifInterface.getRotationForOrientationValue( + exif.getTagIntValue(ExifInterface.TAG_ORIENTATION).shortValue()); } catch (IOException e) { - e.printStackTrace(); - } catch (ExifInvalidFormatException e) { - e.printStackTrace(); - } finally { - Utils.closeSilently(is); + Log.w(LOGTAG, "Failed to read EXIF orientation", e); } return orientation; } @@ -196,9 +184,9 @@ public class ImageLoader { return -1; } } catch (SQLiteException e) { - return ExifInterface.ORIENTATION_UNDEFINED; + return -1; } catch (IllegalArgumentException e) { - return ExifInterface.ORIENTATION_UNDEFINED; + return -1; } finally { Utils.closeSilently(cursor); } diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java index 215d5d438..f6c3bdd89 100644 --- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java +++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java @@ -61,16 +61,6 @@ public abstract class BaseFiltersManager { return null; } - public void resetBitmapsRS() { - for (Class c : mFilters.keySet()) { - ImageFilter filter = mFilters.get(c); - if (filter instanceof ImageFilterRS) { - ImageFilterRS filterRS = (ImageFilterRS) filter; - filterRS.resetBitmap(); - } - } - } - public void freeFilterResources(ImagePreset preset) { if (preset == null) { return; diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java index 978bc8bd1..595aa9b30 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java @@ -21,72 +21,76 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.support.v8.renderscript.*; import android.util.Log; +import android.content.res.Resources; import com.android.gallery3d.R; public abstract class ImageFilterRS extends ImageFilter { - private final String LOGTAG = "ImageFilterRS"; + private static final String LOGTAG = "ImageFilterRS"; - private static RenderScript mRS = null; - protected static Allocation mInPixelsAllocation; - protected static Allocation mOutPixelsAllocation; - private static android.content.res.Resources mResources = null; - private static Bitmap sOldBitmap = null; - private Bitmap mOldBitmap = null; + protected static volatile Allocation mInPixelsAllocation; + protected static volatile Allocation mOutPixelsAllocation; - private boolean mResourcesLoaded = false; + private static volatile RenderScript sRS = null; + private static volatile int sWidth = 0; + private static volatile int sHeight = 0; - private final Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888; + private static volatile Resources sResources = null; + private boolean mResourcesLoaded = false; - public void resetBitmap() { - mOldBitmap = null; - } + private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; + // This must be used inside block synchronized on ImageFilterRS class object public void prepare(Bitmap bitmap, float scaleFactor, int quality) { - if (sOldBitmap == null - || (bitmap.getWidth() != sOldBitmap.getWidth()) - || (bitmap.getHeight() != sOldBitmap.getHeight())) { - if (mInPixelsAllocation != null) { - mInPixelsAllocation.destroy(); - mInPixelsAllocation = null; + if (mOutPixelsAllocation == null || mInPixelsAllocation == null || + bitmap.getWidth() != sWidth || bitmap.getHeight() != sHeight) { + destroyPixelAllocations(); + Bitmap bitmapBuffer = bitmap; + if (bitmap.getConfig() == null || bitmap.getConfig() != BITMAP_CONFIG) { + bitmapBuffer = bitmap.copy(BITMAP_CONFIG, true); } - if (mOutPixelsAllocation != null) { - mOutPixelsAllocation.destroy(); - mOutPixelsAllocation = null; - } - Bitmap bitmapBuffer = bitmap.copy(mBitmapConfig, true); - mOutPixelsAllocation = Allocation.createFromBitmap(mRS, bitmapBuffer, + mOutPixelsAllocation = Allocation.createFromBitmap(sRS, bitmapBuffer, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); - mInPixelsAllocation = Allocation.createTyped(mRS, + mInPixelsAllocation = Allocation.createTyped(sRS, mOutPixelsAllocation.getType()); - sOldBitmap = bitmap; } mInPixelsAllocation.copyFrom(bitmap); - if (mOldBitmap != sOldBitmap || !isResourcesLoaded()) { + if (bitmap.getWidth() != sWidth + || bitmap.getHeight() != sHeight || !isResourcesLoaded()) { freeResources(); - createFilter(mResources, scaleFactor, quality); - mOldBitmap = sOldBitmap; + createFilter(sResources, scaleFactor, quality); + sWidth = bitmap.getWidth(); + sHeight = bitmap.getHeight(); setResourcesLoaded(true); } } + // This must be used inside block synchronized on ImageFilterRS class object abstract public void createFilter(android.content.res.Resources res, float scaleFactor, int quality); + // This must be used inside block synchronized on ImageFilterRS class object abstract public void runFilter(); + // This must be used inside block synchronized on ImageFilterRS class object public void update(Bitmap bitmap) { mOutPixelsAllocation.copyTo(bitmap); } @Override public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { - if (bitmap == null) { + if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) { return bitmap; } try { - prepare(bitmap, scaleFactor, quality); - runFilter(); - update(bitmap); + synchronized(ImageFilterRS.class) { + if (sRS == null) { + Log.w(LOGTAG, "Cannot apply before calling setRenderScriptContext"); + return bitmap; + } + prepare(bitmap, scaleFactor, quality); + runFilter(); + update(bitmap); + } } catch (android.renderscript.RSIllegalArgumentException e) { Log.e(LOGTAG, "Illegal argument? " + e); } catch (android.renderscript.RSRuntimeException e) { @@ -100,15 +104,21 @@ public abstract class ImageFilterRS extends ImageFilter { return bitmap; } - public static RenderScript getRenderScriptContext() { - return mRS; + public static synchronized RenderScript getRenderScriptContext() { + return sRS; } - public static void setRenderScriptContext(Activity context) { - if (mRS == null) { - mRS = RenderScript.create(context); + public static synchronized void setRenderScriptContext(Activity context) { + if( sRS != null) { + Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext"); + destroyRenderScriptContext(); } - mResources = context.getResources(); + sRS = RenderScript.create(context); + sResources = context.getResources(); + destroyPixelAllocations(); + } + + private static synchronized void destroyPixelAllocations() { if (mInPixelsAllocation != null) { mInPixelsAllocation.destroy(); mInPixelsAllocation = null; @@ -117,36 +127,49 @@ public abstract class ImageFilterRS extends ImageFilter { mOutPixelsAllocation.destroy(); mOutPixelsAllocation = null; } - sOldBitmap = null; + sWidth = 0; + sHeight = 0; + } + + public static synchronized void destroyRenderScriptContext() { + destroyPixelAllocations(); + sRS.destroy(); + sRS = null; + sResources = null; } - private Allocation convertBitmap(Bitmap bitmap) { - return Allocation.createFromBitmap(mRS, bitmap, + private static synchronized Allocation convertBitmap(Bitmap bitmap) { + return Allocation.createFromBitmap(sRS, bitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); } - private Allocation convertRGBAtoA(Bitmap bitmap) { - Type.Builder tb_a8 = new Type.Builder(mRS, Element.U8(mRS)); - ScriptC_grey greyConvert = new ScriptC_grey(mRS, mResources, R.raw.grey); + private static synchronized Allocation convertRGBAtoA(Bitmap bitmap) { + Type.Builder tb_a8 = new Type.Builder(sRS, Element.U8(sRS)); + ScriptC_grey greyConvert = new ScriptC_grey(sRS, + sRS.getApplicationContext().getResources(), R.raw.grey); Allocation bitmapTemp = convertBitmap(bitmap); - if (bitmapTemp.getType().getElement().isCompatible(Element.U8(mRS))) { + if (bitmapTemp.getType().getElement().isCompatible(Element.U8(sRS))) { return bitmapTemp; } tb_a8.setX(bitmapTemp.getType().getX()); tb_a8.setY(bitmapTemp.getType().getY()); - Allocation bitmapAlloc = Allocation.createTyped(mRS, tb_a8.create()); + Allocation bitmapAlloc = Allocation.createTyped(sRS, tb_a8.create()); greyConvert.forEach_RGBAtoA(bitmapTemp, bitmapAlloc); return bitmapAlloc; } public Allocation loadResourceAlpha(int resource) { + Resources res = null; + synchronized(ImageFilterRS.class) { + res = sRS.getApplicationContext().getResources(); + } final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ALPHA_8; Bitmap bitmap = BitmapFactory.decodeResource( - mRS.getApplicationContext().getResources(), + res, resource, options); Allocation ret = convertRGBAtoA(bitmap); bitmap.recycle(); @@ -154,21 +177,25 @@ public abstract class ImageFilterRS extends ImageFilter { } public Allocation loadResource(int resource) { + Resources res = null; + synchronized(ImageFilterRS.class) { + res = sRS.getApplicationContext().getResources(); + } final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ARGB_8888; Bitmap bitmap = BitmapFactory.decodeResource( - mRS.getApplicationContext().getResources(), + res, resource, options); Allocation ret = convertBitmap(bitmap); bitmap.recycle(); return ret; } - public boolean isResourcesLoaded() { + private boolean isResourcesLoaded() { return mResourcesLoaded; } - public void setResourcesLoaded(boolean resourcesLoaded) { + private void setResourcesLoaded(boolean resourcesLoaded) { mResourcesLoaded = resourcesLoaded; } diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java index b88dbbc47..aa7e70065 100644 --- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java +++ b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java @@ -21,7 +21,6 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; @@ -31,21 +30,15 @@ import android.provider.MediaStore.Images.ImageColumns; import android.util.Log; import com.android.gallery3d.common.Utils; -import com.android.gallery3d.exif.ExifData; -import com.android.gallery3d.exif.ExifInvalidFormatException; -import com.android.gallery3d.exif.ExifOutputStream; -import com.android.gallery3d.exif.ExifReader; -import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.filtershow.cache.ImageLoader; import com.android.gallery3d.filtershow.presets.ImagePreset; import com.android.gallery3d.util.XmpUtilHelper; import java.io.File; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.sql.Date; import java.text.SimpleDateFormat; import java.util.TimeZone; @@ -55,35 +48,7 @@ import java.util.TimeZone; */ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { - private static final String LOGTAG = "SaveCopyTask"; - /** - * Saves the bitmap in the final destination - */ - public static void saveBitmap(Bitmap bitmap, File destination, Object xmp) { - saveBitmap(bitmap, destination, xmp, null); - } - - private static void saveBitmap(Bitmap bitmap, File destination, Object xmp, ExifData exif) { - OutputStream os = null; - try { - os = new FileOutputStream(destination); - if (exif != null) { - ExifOutputStream eos = new ExifOutputStream(os); - eos.setExifData(exif); - bitmap.compress(CompressFormat.JPEG, ImageLoader.DEFAULT_COMPRESS_QUALITY, eos); - } else { - bitmap.compress(CompressFormat.JPEG, ImageLoader.DEFAULT_COMPRESS_QUALITY, os); - } - } catch (FileNotFoundException e) { - Log.v(LOGTAG,"Error in writing "+destination.getAbsolutePath()); - } finally { - Utils.closeSilently(os);; - } - if (xmp != null) { - XmpUtilHelper.writeXMPMeta(destination.getAbsolutePath(), xmp); - } - } /** * Callback for the completed asynchronous task. @@ -128,7 +93,8 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { ImageLoader.DEFAULT_SAVE_DIRECTORY); } // Create the directory if it doesn't exist - if (!saveDirectory.exists()) saveDirectory.mkdirs(); + if (!saveDirectory.exists()) + saveDirectory.mkdirs(); return saveDirectory; } @@ -139,28 +105,59 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { return new File(saveDirectory, filename + ".JPG"); } - private ExifData getExifData(Uri sourceUri) { + public Object getPanoramaXMPData(Uri source, ImagePreset preset) { + Object xmp = null; + if (preset.isPanoramaSafe()) { + InputStream is = null; + try { + is = context.getContentResolver().openInputStream(source); + xmp = XmpUtilHelper.extractXMPMeta(is); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "Failed to get XMP data from image: ", e); + } finally { + Utils.closeSilently(is); + } + } + return xmp; + } + + public boolean putPanoramaXMPData(File file, Object xmp) { + if (xmp != null) { + return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp); + } + return false; + } + + public ExifInterface getExifData(Uri source) { + ExifInterface exif = new ExifInterface(); String mimeType = context.getContentResolver().getType(sourceUri); - if (mimeType != ImageLoader.JPEG_MIME_TYPE) { - return null; + if (mimeType == ImageLoader.JPEG_MIME_TYPE) { + InputStream inStream = null; + try { + inStream = context.getContentResolver().openInputStream(source); + exif.readExif(inStream); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "Cannot find file: " + source, e); + } catch (IOException e) { + Log.w(LOGTAG, "Cannot read exif for: " + source, e); + } finally { + Utils.closeSilently(inStream); + } } - InputStream is = null; + return exif; + } + + public boolean putExifData(File file, ExifInterface exif, Bitmap image) { + boolean ret = false; try { - is = context.getContentResolver().openInputStream(sourceUri); - ExifReader reader = new ExifReader(); - return reader.read(is); + exif.writeExif(image, file.getAbsolutePath()); + ret = true; } catch (FileNotFoundException e) { - Log.w(LOGTAG, "Failed to find file", e); - return null; - } catch (ExifInvalidFormatException e) { - Log.w(LOGTAG, "Invalid EXIF data", e); - return null; + Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e); } catch (IOException e) { - Log.w(LOGTAG, "Failed to read original file", e); - return null; - } finally { - Utils.closeSilently(is); + Log.w(LOGTAG, "Could not write exif: ", e); } + return ret; } /** @@ -173,12 +170,12 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { return null; } ImagePreset preset = params[0]; - InputStream is = null; BitmapFactory.Options options = new BitmapFactory.Options(); + Uri uri = null; boolean noBitmap = true; int num_tries = 0; // Stopgap fix for low-memory devices. - while(noBitmap) { + while (noBitmap) { try { // Try to do bitmap operations, downsample if low-memory Bitmap bitmap = ImageLoader.loadMutableBitmap(context, sourceUri, options); @@ -189,24 +186,23 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { bitmap = preset.applyGeometry(bitmap); bitmap = preset.apply(bitmap); - Object xmp = null; - if (preset.isPanoramaSafe()) { - is = context.getContentResolver().openInputStream(sourceUri); - xmp = XmpUtilHelper.extractXMPMeta(is); - } - ExifData exif = getExifData(sourceUri); - if (exif != null) { - exif.addDateTimeStampTag(ExifTag.TAG_DATE_TIME, System.currentTimeMillis(), - TimeZone.getDefault()); - // Since the image has been modified, set the orientation to normal. - exif.addTag(ExifTag.TAG_ORIENTATION).setValue(ExifTag.Orientation.TOP_LEFT); + Object xmp = getPanoramaXMPData(sourceUri, preset); + ExifInterface exif = getExifData(sourceUri); + + // Set tags + long time = System.currentTimeMillis(); + exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time, + TimeZone.getDefault()); + exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, + ExifInterface.Orientation.TOP_LEFT)); + + // If we succeed in writing the bitmap as a jpeg, return a uri. + if (putExifData(this.destinationFile, exif, bitmap)) { + putPanoramaXMPData(this.destinationFile, xmp); + uri = insertContent(context, sourceUri, this.destinationFile, saveFileName, + time); } - saveBitmap(bitmap, this.destinationFile, xmp, exif); - bitmap.recycle(); noBitmap = false; - } catch (FileNotFoundException ex) { - Log.w(LOGTAG, "Failed to save image!", ex); - return null; } catch (java.lang.OutOfMemoryError e) { // Try 5 times before failing for good. if (++num_tries >= 5) { @@ -214,13 +210,9 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { } System.gc(); options.inSampleSize *= 2; - } finally { - Utils.closeSilently(is); } } - Uri uri = insertContent(context, sourceUri, this.destinationFile, saveFileName); return uri; - } @Override @@ -267,16 +259,17 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { /** * Insert the content (saved file) with proper source photo properties. */ - public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName) { - long now = System.currentTimeMillis() / 1000; + public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName, + long time) { + time /= 1000; final ContentValues values = new ContentValues(); values.put(Images.Media.TITLE, saveFileName); values.put(Images.Media.DISPLAY_NAME, file.getName()); values.put(Images.Media.MIME_TYPE, "image/jpeg"); - values.put(Images.Media.DATE_TAKEN, now); - values.put(Images.Media.DATE_MODIFIED, now); - values.put(Images.Media.DATE_ADDED, now); + values.put(Images.Media.DATE_TAKEN, time); + values.put(Images.Media.DATE_MODIFIED, time); + values.put(Images.Media.DATE_ADDED, time); values.put(Images.Media.ORIENTATION, 0); values.put(Images.Media.DATA, file.getAbsolutePath()); values.put(Images.Media.SIZE, file.length()); @@ -288,20 +281,20 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { querySource(context, sourceUri, projection, new ContentResolverQueryCallback() { - @Override - public void onCursorResult(Cursor cursor) { - values.put(Images.Media.DATE_TAKEN, cursor.getLong(0)); - - double latitude = cursor.getDouble(1); - double longitude = cursor.getDouble(2); - // TODO: Change || to && after the default location issue is - // fixed. - if ((latitude != 0f) || (longitude != 0f)) { - values.put(Images.Media.LATITUDE, latitude); - values.put(Images.Media.LONGITUDE, longitude); - } - } - }); + @Override + public void onCursorResult(Cursor cursor) { + values.put(Images.Media.DATE_TAKEN, cursor.getLong(0)); + + double latitude = cursor.getDouble(1); + double longitude = cursor.getDouble(2); + // TODO: Change || to && after the default location + // issue is fixed. + if ((latitude != 0f) || (longitude != 0f)) { + values.put(Images.Media.LATITUDE, latitude); + values.put(Images.Media.LONGITUDE, longitude); + } + } + }); return context.getContentResolver().insert( Images.Media.EXTERNAL_CONTENT_URI, values); diff --git a/src/com/android/photos/AlbumSetFragment.java b/src/com/android/photos/AlbumSetFragment.java index 0d4fcc023..d0bc81fd6 100644 --- a/src/com/android/photos/AlbumSetFragment.java +++ b/src/com/android/photos/AlbumSetFragment.java @@ -21,42 +21,54 @@ import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; import android.content.Loader; import android.database.Cursor; -import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Bundle; -import android.text.format.DateFormat; +import android.provider.MediaStore.Files.FileColumns; +import android.util.SparseBooleanArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; -import android.widget.CursorAdapter; import android.widget.GridView; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; import android.widget.Toast; import com.android.gallery3d.R; +import com.android.photos.adapters.AlbumSetCursorAdapter; import com.android.photos.data.AlbumSetLoader; import com.android.photos.shims.LoaderCompatShim; import com.android.photos.shims.MediaSetLoader; -import java.util.Date; +import java.util.ArrayList; public class AlbumSetFragment extends Fragment implements OnItemClickListener, - LoaderCallbacks<Cursor> { + LoaderCallbacks<Cursor>, MultiChoiceManager.Delegate, SelectionManager.Client { private GridView mAlbumSetView; private View mEmptyView; private AlbumSetCursorAdapter mAdapter; + private LoaderCompatShim<Cursor> mLoaderCompatShim; + private MultiChoiceManager mMultiChoiceManager; + private SelectionManager mSelectionManager; private static final int LOADER_ALBUMSET = 1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mAdapter = new AlbumSetCursorAdapter(getActivity()); + Context context = getActivity(); + mAdapter = new AlbumSetCursorAdapter(context); + mMultiChoiceManager = new MultiChoiceManager(context, this); + mMultiChoiceManager.setSelectionManager(mSelectionManager); + } + + @Override + public void setSelectionManager(SelectionManager manager) { + mSelectionManager = manager; + if (mMultiChoiceManager != null) { + mMultiChoiceManager.setSelectionManager(manager); + } } @Override @@ -67,6 +79,8 @@ public class AlbumSetFragment extends Fragment implements OnItemClickListener, mEmptyView = root.findViewById(android.R.id.empty); mEmptyView.setVisibility(View.GONE); mAlbumSetView.setAdapter(mAdapter); + mAlbumSetView.setChoiceMode(GridView.CHOICE_MODE_MULTIPLE_MODAL); + mAlbumSetView.setMultiChoiceModeListener(mMultiChoiceManager); mAlbumSetView.setOnItemClickListener(this); getLoaderManager().initLoader(LOADER_ALBUMSET, null, this); updateEmptyStatus(); @@ -78,6 +92,7 @@ public class AlbumSetFragment extends Fragment implements OnItemClickListener, // TODO: Switch to AlbumSetLoader MediaSetLoader loader = new MediaSetLoader(getActivity()); mAdapter.setDrawableFactory(loader); + mLoaderCompatShim = loader; return loader; } @@ -100,63 +115,54 @@ public class AlbumSetFragment extends Fragment implements OnItemClickListener, @Override public void onItemClick(AdapterView<?> av, View v, int pos, long id) { - Cursor c = (Cursor) av.getItemAtPosition(pos); - int albumId = c.getInt(AlbumSetLoader.INDEX_ID); - // TODO launch an activity showing the photos in the album - Toast.makeText(v.getContext(), "Clicked " + albumId, Toast.LENGTH_SHORT).show(); + if (mLoaderCompatShim == null) { + // Not fully initialized yet, discard + return; + } + Cursor item = (Cursor) mAdapter.getItem(pos); + Toast.makeText(v.getContext(), + "Tapped " + item.getInt(AlbumSetLoader.INDEX_ID), + Toast.LENGTH_SHORT).show(); } - private static class AlbumSetCursorAdapter extends CursorAdapter { + @Override + public int getItemMediaType(Object item) { + return FileColumns.MEDIA_TYPE_NONE; + } - private LoaderCompatShim<Cursor> mDrawableFactory; + @Override + public int getItemSupportedOperations(Object item) { + return ((Cursor) item).getInt(AlbumSetLoader.INDEX_SUPPORTED_OPERATIONS); + } - public void setDrawableFactory(LoaderCompatShim<Cursor> factory) { - mDrawableFactory = factory; - } - private Date mDate = new Date(); // Used for converting timestamps for display + @Override + public Object getItemAtPosition(int position) { + return mAdapter.getItem(position); + } - public AlbumSetCursorAdapter(Context context) { - super(context, null, false); - } + @Override + public ArrayList<Uri> getSubItemUrisForItem(Object item) { + return mLoaderCompatShim.urisForSubItems((Cursor) item); + } - @Override - public void bindView(View v, Context context, Cursor cursor) { - TextView titleTextView = (TextView) v.findViewById( - R.id.album_set_item_title); - titleTextView.setText(cursor.getString(AlbumSetLoader.INDEX_TITLE)); - - TextView dateTextView = (TextView) v.findViewById( - R.id.album_set_item_date); - long timestamp = cursor.getLong(AlbumSetLoader.INDEX_TIMESTAMP); - if (timestamp > 0) { - mDate.setTime(timestamp); - dateTextView.setText(DateFormat.getMediumDateFormat(context).format(mDate)); - } else { - dateTextView.setText(null); - } - - ProgressBar uploadProgressBar = (ProgressBar) v.findViewById( - R.id.album_set_item_upload_progress); - if (cursor.getInt(AlbumSetLoader.INDEX_COUNT_PENDING_UPLOAD) > 0) { - uploadProgressBar.setVisibility(View.VISIBLE); - uploadProgressBar.setProgress(50); - } else { - uploadProgressBar.setVisibility(View.INVISIBLE); - } - - ImageView thumbImageView = (ImageView) v.findViewById( - R.id.album_set_item_image); - Drawable recycle = thumbImageView.getDrawable(); - Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle); - if (recycle != drawable) { - thumbImageView.setImageDrawable(drawable); - } - } + @Override + public Object getPathForItemAtPosition(int position) { + return mLoaderCompatShim.getPathForItem((Cursor) mAdapter.getItem(position)); + } - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return LayoutInflater.from(context).inflate( - R.layout.album_set_item, parent, false); - } + @Override + public void deleteItemWithPath(Object itemPath) { + mLoaderCompatShim.deleteItemWithPath(itemPath); } + + @Override + public SparseBooleanArray getSelectedItemPositions() { + return mAlbumSetView.getCheckedItemPositions(); + } + + @Override + public int getSelectedItemCount() { + return mAlbumSetView.getCheckedItemCount(); + } + } diff --git a/src/com/android/photos/GalleryActivity.java b/src/com/android/photos/GalleryActivity.java index 420fc3d51..ddf04e365 100644 --- a/src/com/android/photos/GalleryActivity.java +++ b/src/com/android/photos/GalleryActivity.java @@ -43,7 +43,7 @@ public class GalleryActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + mSelectionManager = new SelectionManager(this); mViewPager = new ViewPager(this); mViewPager.setId(R.id.viewpager); setContentView(mViewPager); @@ -70,13 +70,6 @@ public class GalleryActivity extends Activity { outState.putInt("tab", getActionBar().getSelectedNavigationIndex()); } - protected SelectionManager getSelectionManager() { - if (mSelectionManager == null) { - mSelectionManager = new SelectionManager(this); - } - return mSelectionManager; - } - @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.gallery, menu); @@ -99,7 +92,7 @@ public class GalleryActivity extends Activity { public static class TabsAdapter extends FragmentPagerAdapter implements ActionBar.TabListener, ViewPager.OnPageChangeListener { - private final Context mContext; + private final GalleryActivity mActivity; private final ActionBar mActionBar; private final ViewPager mViewPager; private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>(); @@ -115,9 +108,9 @@ public class GalleryActivity extends Activity { } } - public TabsAdapter(Activity activity, ViewPager pager) { + public TabsAdapter(GalleryActivity activity, ViewPager pager) { super(activity.getFragmentManager()); - mContext = activity; + mActivity = activity; mActionBar = activity.getActionBar(); mViewPager = pager; mViewPager.setAdapter(this); @@ -141,8 +134,11 @@ public class GalleryActivity extends Activity { @Override public Fragment getItem(int position) { TabInfo info = mTabs.get(position); - return Fragment.instantiate(mContext, info.clss.getName(), + Fragment item = Fragment.instantiate(mActivity, info.clss.getName(), info.args); + ((SelectionManager.Client) item).setSelectionManager( + mActivity.mSelectionManager); + return item; } @Override diff --git a/src/com/android/photos/MultiChoiceManager.java b/src/com/android/photos/MultiChoiceManager.java new file mode 100644 index 000000000..e00c842fe --- /dev/null +++ b/src/com/android/photos/MultiChoiceManager.java @@ -0,0 +1,190 @@ +/* + * 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.photos; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.SparseBooleanArray; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.AbsListView.MultiChoiceModeListener; +import android.widget.ShareActionProvider; +import android.widget.ShareActionProvider.OnShareTargetSelectedListener; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaObject; + +import java.util.ArrayList; +import java.util.List; + +public class MultiChoiceManager implements MultiChoiceModeListener, + OnShareTargetSelectedListener, SelectionManager.SelectedUriSource { + + public interface Delegate { + public SparseBooleanArray getSelectedItemPositions(); + public int getSelectedItemCount(); + public int getItemMediaType(Object item); + public int getItemSupportedOperations(Object item); + public ArrayList<Uri> getSubItemUrisForItem(Object item); + public Object getItemAtPosition(int position); + public Object getPathForItemAtPosition(int position); + public void deleteItemWithPath(Object itemPath); + } + + private SelectionManager mSelectionManager; + private ShareActionProvider mShareActionProvider; + private ActionMode mActionMode; + private int numSubItemsCollected = 0; + private Context mContext; + private Delegate mDelegate; + + private ArrayList<Uri> mSelectedUrisArray = new ArrayList<Uri>(); + + public MultiChoiceManager(Context context, Delegate delegate) { + mContext = context; + mDelegate = delegate; + } + + public void setSelectionManager(SelectionManager selectionManager) { + mSelectionManager = selectionManager; + } + + @Override + public ArrayList<Uri> getSelectedShareableUris() { + return mSelectedUrisArray; + } + + private void updateSelectedTitle(ActionMode mode) { + int count = mDelegate.getSelectedItemCount(); + mode.setTitle(mContext.getResources().getQuantityString( + R.plurals.number_of_items_selected, count, count)); + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + updateSelectedTitle(mode); + Object item = mDelegate.getItemAtPosition(position); + + ArrayList<Uri> subItems = mDelegate.getSubItemUrisForItem(item); + if (checked) { + mSelectedUrisArray.addAll(subItems); + numSubItemsCollected += subItems.size(); + } else { + mSelectedUrisArray.removeAll(subItems); + numSubItemsCollected -= subItems.size(); + } + + mSelectionManager.onItemSelectedStateChanged(mShareActionProvider, + mDelegate.getItemMediaType(item), + mDelegate.getItemSupportedOperations(item), + checked); + updateActionItemVisibilities(mode.getMenu(), + mSelectionManager.getSupportedOperations()); + } + + private void updateActionItemVisibilities(Menu menu, int supportedOperations) { + MenuItem shareItem = menu.findItem(R.id.menu_share); + MenuItem deleteItem = menu.findItem(R.id.menu_delete); + shareItem.setVisible((supportedOperations & MediaObject.SUPPORT_SHARE) > 0); + deleteItem.setVisible((supportedOperations & MediaObject.SUPPORT_DELETE) > 0); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mSelectionManager.setSelectedUriSource(this); + mActionMode = mode; + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.gallery_multiselect, menu); + MenuItem menuItem = menu.findItem(R.id.menu_share); + mShareActionProvider = (ShareActionProvider) menuItem.getActionProvider(); + mShareActionProvider.setOnShareTargetSelectedListener(this); + updateSelectedTitle(mode); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + // onDestroyActionMode gets called when the share target was selected, + // but apparently before the ArrayList is serialized in the intent + // so we can't clear the old one here. + mSelectedUrisArray = new ArrayList<Uri>(); + mSelectionManager.onClearSelection(); + mSelectionManager.setSelectedUriSource(null); + mShareActionProvider = null; + mActionMode = null; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + updateSelectedTitle(mode); + return false; + } + + @Override + public boolean onShareTargetSelected(ShareActionProvider provider, Intent intent) { + mActionMode.finish(); + return false; + } + + private static class BulkDeleteTask extends AsyncTask<Void, Void, Void> { + private Delegate mDelegate; + private List<Object> mPaths; + + public BulkDeleteTask(Delegate delegate, List<Object> paths) { + mDelegate = delegate; + mPaths = paths; + } + + @Override + protected Void doInBackground(Void... ignored) { + for (Object path : mPaths) { + mDelegate.deleteItemWithPath(path); + } + return null; + } + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_delete: + BulkDeleteTask deleteTask = new BulkDeleteTask(mDelegate, + getPathsForSelectedItems()); + deleteTask.execute(); + mode.finish(); + return true; + default: + return false; + } + } + + private List<Object> getPathsForSelectedItems() { + List<Object> paths = new ArrayList<Object>(); + SparseBooleanArray selected = mDelegate.getSelectedItemPositions(); + for (int i = 0; i < selected.size(); i++) { + if (selected.valueAt(i)) { + paths.add(mDelegate.getPathForItemAtPosition(i)); + } + } + return paths; + } +} diff --git a/src/com/android/photos/PhotoSetFragment.java b/src/com/android/photos/PhotoSetFragment.java index 25d80360d..b485cd051 100644 --- a/src/com/android/photos/PhotoSetFragment.java +++ b/src/com/android/photos/PhotoSetFragment.java @@ -18,41 +18,31 @@ package com.android.photos; import android.app.Fragment; import android.app.LoaderManager.LoaderCallbacks; +import android.content.Context; import android.content.Intent; import android.content.Loader; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.util.SparseBooleanArray; -import android.view.ActionMode; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.GridView; -import android.widget.ShareActionProvider; -import android.widget.ShareActionProvider.OnShareTargetSelectedListener; import com.android.gallery3d.R; import com.android.gallery3d.app.Gallery; -import com.android.gallery3d.data.MediaItem; import com.android.photos.adapters.PhotoThumbnailAdapter; import com.android.photos.data.PhotoSetLoader; import com.android.photos.shims.LoaderCompatShim; import com.android.photos.shims.MediaItemsLoader; import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; -public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor>, - OnItemClickListener, SelectionManager.SelectedUriSource, MultiChoiceModeListener, - OnShareTargetSelectedListener { +public class PhotoSetFragment extends Fragment implements OnItemClickListener, + LoaderCallbacks<Cursor>, MultiChoiceManager.Delegate, SelectionManager.Client { private static final int LOADER_PHOTOSET = 1; @@ -62,14 +52,24 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor private boolean mInitialLoadComplete = false; private LoaderCompatShim<Cursor> mLoaderCompatShim; private PhotoThumbnailAdapter mAdapter; + private MultiChoiceManager mMultiChoiceManager; private SelectionManager mSelectionManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - GalleryActivity activity = (GalleryActivity) getActivity(); - mSelectionManager = activity.getSelectionManager(); - mAdapter = new PhotoThumbnailAdapter(activity); + Context context = getActivity(); + mAdapter = new PhotoThumbnailAdapter(context); + mMultiChoiceManager = new MultiChoiceManager(context, this); + mMultiChoiceManager.setSelectionManager(mSelectionManager); + } + + @Override + public void setSelectionManager(SelectionManager manager) { + mSelectionManager = manager; + if (mMultiChoiceManager != null) { + mMultiChoiceManager.setSelectionManager(manager); + } } @Override @@ -84,7 +84,7 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor mEmptyView.setVisibility(View.GONE); mPhotoSetView.setAdapter(mAdapter); mPhotoSetView.setChoiceMode(GridView.CHOICE_MODE_MULTIPLE_MODAL); - mPhotoSetView.setMultiChoiceModeListener(this); + mPhotoSetView.setMultiChoiceModeListener(mMultiChoiceManager); getLoaderManager().initLoader(LOADER_PHOTOSET, null, this); updateEmptyStatus(); return root; @@ -129,112 +129,51 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor updateEmptyStatus(); } - private Set<Uri> mSelectedUris = new HashSet<Uri>(); - private ArrayList<Uri> mSelectedUrisArray = new ArrayList<Uri>(); - @Override - public ArrayList<Uri> getSelectedShareableUris() { - mSelectedUrisArray.clear(); - mSelectedUrisArray.addAll(mSelectedUris); - return mSelectedUrisArray; - } - - public ArrayList<Uri> getSelectedShareableUrisUncached() { - mSelectedUrisArray.clear(); - SparseBooleanArray selected = mPhotoSetView.getCheckedItemPositions(); - - for (int i = 0; i < selected.size(); i++) { - if (selected.valueAt(i)) { - Cursor item = mAdapter.getItem(selected.keyAt(i)); - int supported = item.getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS); - if ((supported & MediaItem.SUPPORT_SHARE) > 0) { - mSelectedUrisArray.add(mLoaderCompatShim.uriForItem(item)); - } - } - } - - return mSelectedUrisArray; + public void onLoaderReset(Loader<Cursor> loader) { } @Override - public void onLoaderReset(Loader<Cursor> loader) { + public int getItemMediaType(Object item) { + return ((Cursor) item).getInt(PhotoSetLoader.INDEX_MEDIA_TYPE); } - - private ShareActionProvider mShareActionProvider; - private ActionMode mActionMode; - private boolean mSharePending = false; - - private void updateSelectedTitle(ActionMode mode) { - int count = mPhotoSetView.getCheckedItemCount(); - mode.setTitle(getResources().getQuantityString( - R.plurals.number_of_items_selected, count, count)); + @Override + public int getItemSupportedOperations(Object item) { + return ((Cursor) item).getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS); } @Override - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { - updateSelectedTitle(mode); - Cursor item = mAdapter.getItem(position); - - if (checked) { - mSelectedUris.add(mLoaderCompatShim.uriForItem(item)); - } else { - mSelectedUris.remove(mLoaderCompatShim.uriForItem(item)); - } - - mSelectionManager.onItemSelectedStateChanged(mShareActionProvider, - item.getInt(PhotoSetLoader.INDEX_MEDIA_TYPE), - item.getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS), - checked); + public Object getItemAtPosition(int position) { + return mAdapter.getItem(position); } + private ArrayList<Uri> mSubItemUriTemp = new ArrayList<Uri>(1); @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mSelectionManager.setSelectedUriSource(PhotoSetFragment.this); - mActionMode = mode; - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.gallery_multiselect, menu); - MenuItem menuItem = menu.findItem(R.id.menu_share); - mShareActionProvider = (ShareActionProvider) menuItem.getActionProvider(); - mShareActionProvider.setOnShareTargetSelectedListener(this); - updateSelectedTitle(mode); - return true; + public ArrayList<Uri> getSubItemUrisForItem(Object item) { + mSubItemUriTemp.clear(); + mSubItemUriTemp.add(mLoaderCompatShim.uriForItem((Cursor) item)); + return mSubItemUriTemp; } + @Override - public void onDestroyActionMode(ActionMode mode) { - mSelectedUris.clear(); - if (mSharePending) { - // onDestroyActionMode gets called when the share target was selected, - // but apparently before the ArrayList is serialized in the intent - // so we can't clear the old one here. - mSelectedUrisArray = new ArrayList<Uri>(); - mSharePending = false; - } else { - mSelectedUrisArray.clear(); - } - mSelectionManager.onClearSelection(); - mSelectionManager.setSelectedUriSource(null); - mShareActionProvider = null; - mActionMode = null; + public Object getPathForItemAtPosition(int position) { + return mLoaderCompatShim.getPathForItem(mAdapter.getItem(position)); } @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - updateSelectedTitle(mode); - return false; + public void deleteItemWithPath(Object itemPath) { + mLoaderCompatShim.deleteItemWithPath(itemPath); } @Override - public boolean onShareTargetSelected(ShareActionProvider provider, Intent intent) { - mSharePending = true; - mActionMode.finish(); - return false; + public SparseBooleanArray getSelectedItemPositions() { + return mPhotoSetView.getCheckedItemPositions(); } @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; + public int getSelectedItemCount() { + return mPhotoSetView.getCheckedItemCount(); } } diff --git a/src/com/android/photos/SelectionManager.java b/src/com/android/photos/SelectionManager.java index 979dcc7da..ce340c731 100644 --- a/src/com/android/photos/SelectionManager.java +++ b/src/com/android/photos/SelectionManager.java @@ -26,7 +26,7 @@ import android.provider.MediaStore.Files.FileColumns; import android.widget.ShareActionProvider; import com.android.gallery3d.common.ApiHelper; -import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; import com.android.gallery3d.util.GalleryUtils; import java.util.ArrayList; @@ -41,6 +41,10 @@ public class SelectionManager { public ArrayList<Uri> getSelectedShareableUris(); } + public interface Client { + public void setSelectionManager(SelectionManager manager); + } + public SelectionManager(Activity activity) { mActivity = activity; if (ApiHelper.AT_LEAST_16) { @@ -76,10 +80,10 @@ public class SelectionManager { mSelectedTotalCount += increment; mCachedShareableUris = null; - if ((itemSupportedOperations & MediaItem.SUPPORT_DELETE) > 0) { + if ((itemSupportedOperations & MediaObject.SUPPORT_DELETE) > 0) { mSelectedDeletableCount += increment; } - if ((itemSupportedOperations & MediaItem.SUPPORT_SHARE) > 0) { + if ((itemSupportedOperations & MediaObject.SUPPORT_SHARE) > 0) { mSelectedShareableCount += increment; if (itemType == FileColumns.MEDIA_TYPE_IMAGE) { mSelectedShareableImageCount += increment; @@ -93,24 +97,42 @@ public class SelectionManager { mShareIntent.setAction(null).setType(null); } else if (mSelectedShareableCount >= 1) { mCachedShareableUris = mUriSource.getSelectedShareableUris(); - if (mSelectedShareableImageCount == mSelectedShareableCount) { - mShareIntent.setType(GalleryUtils.MIME_TYPE_IMAGE); - } else if (mSelectedShareableVideoCount == mSelectedShareableCount) { - mShareIntent.setType(GalleryUtils.MIME_TYPE_VIDEO); - } else { - mShareIntent.setType(GalleryUtils.MIME_TYPE_ALL); - } - if (mSelectedShareableCount == 1) { - mShareIntent.setAction(Intent.ACTION_SEND); - mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris.get(0)); + if (mCachedShareableUris.size() == 0) { + mShareIntent.setAction(null).setType(null); } else { - mShareIntent.setAction(Intent.ACTION_SEND_MULTIPLE); - mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris); + if (mSelectedShareableImageCount == mSelectedShareableCount) { + mShareIntent.setType(GalleryUtils.MIME_TYPE_IMAGE); + } else if (mSelectedShareableVideoCount == mSelectedShareableCount) { + mShareIntent.setType(GalleryUtils.MIME_TYPE_VIDEO); + } else { + mShareIntent.setType(GalleryUtils.MIME_TYPE_ALL); + } + if (mCachedShareableUris.size() == 1) { + mShareIntent.setAction(Intent.ACTION_SEND); + mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris.get(0)); + } else { + mShareIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris); + } } } share.setShareIntent(mShareIntent); - // TODO update deletability, editability, etc. + // TODO update editability, etc. + } + + public int getSupportedOperations() { + if (mSelectedTotalCount == 0) { + return 0; + } + int supported = 0; + if (mSelectedDeletableCount == mSelectedTotalCount) { + supported |= MediaObject.SUPPORT_DELETE; + } + if (mSelectedShareableCount > 0) { + supported |= MediaObject.SUPPORT_SHARE; + } + return supported; } public void onClearSelection() { diff --git a/src/com/android/photos/adapters/AlbumSetCursorAdapter.java b/src/com/android/photos/adapters/AlbumSetCursorAdapter.java new file mode 100644 index 000000000..c387f8f47 --- /dev/null +++ b/src/com/android/photos/adapters/AlbumSetCursorAdapter.java @@ -0,0 +1,89 @@ +/* + * 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.photos.adapters; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.android.gallery3d.R; +import com.android.photos.data.AlbumSetLoader; +import com.android.photos.shims.LoaderCompatShim; + +import java.util.Date; + +public class AlbumSetCursorAdapter extends CursorAdapter { + + private LoaderCompatShim<Cursor> mDrawableFactory; + + public void setDrawableFactory(LoaderCompatShim<Cursor> factory) { + mDrawableFactory = factory; + } + private Date mDate = new Date(); // Used for converting timestamps for display + + public AlbumSetCursorAdapter(Context context) { + super(context, null, false); + } + + @Override + public void bindView(View v, Context context, Cursor cursor) { + TextView titleTextView = (TextView) v.findViewById( + R.id.album_set_item_title); + titleTextView.setText(cursor.getString(AlbumSetLoader.INDEX_TITLE)); + + TextView dateTextView = (TextView) v.findViewById( + R.id.album_set_item_date); + long timestamp = cursor.getLong(AlbumSetLoader.INDEX_TIMESTAMP); + if (timestamp > 0) { + mDate.setTime(timestamp); + dateTextView.setText(DateFormat.getMediumDateFormat(context).format(mDate)); + } else { + dateTextView.setText(null); + } + + ProgressBar uploadProgressBar = (ProgressBar) v.findViewById( + R.id.album_set_item_upload_progress); + if (cursor.getInt(AlbumSetLoader.INDEX_COUNT_PENDING_UPLOAD) > 0) { + uploadProgressBar.setVisibility(View.VISIBLE); + uploadProgressBar.setProgress(50); + } else { + uploadProgressBar.setVisibility(View.INVISIBLE); + } + + ImageView thumbImageView = (ImageView) v.findViewById( + R.id.album_set_item_image); + Drawable recycle = thumbImageView.getDrawable(); + Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle); + if (recycle != drawable) { + thumbImageView.setImageDrawable(drawable); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(context).inflate( + R.layout.album_set_item, parent, false); + } +} diff --git a/src/com/android/photos/data/AlbumSetLoader.java b/src/com/android/photos/data/AlbumSetLoader.java index b2b5204e6..940473255 100644 --- a/src/com/android/photos/data/AlbumSetLoader.java +++ b/src/com/android/photos/data/AlbumSetLoader.java @@ -12,6 +12,7 @@ public class AlbumSetLoader { public static final int INDEX_THUMBNAIL_HEIGHT = 5; public static final int INDEX_COUNT_PENDING_UPLOAD = 6; public static final int INDEX_COUNT = 7; + public static final int INDEX_SUPPORTED_OPERATIONS = 8; public static final String[] PROJECTION = { "_id", @@ -21,7 +22,8 @@ public class AlbumSetLoader { "thumb_width", "thumb_height", "count_pending_upload", - "_count" + "_count", + "supported_operations" }; public static final MatrixCursor MOCK = createRandomCursor(30); @@ -44,7 +46,8 @@ public class AlbumSetLoader { 0, 0, (random < .3 ? 1 : 0), - 1 + 1, + 0 }; return row; } diff --git a/src/com/android/photos/data/PhotoSetLoader.java b/src/com/android/photos/data/PhotoSetLoader.java index 72c8e93cc..56c82c4a9 100644 --- a/src/com/android/photos/data/PhotoSetLoader.java +++ b/src/com/android/photos/data/PhotoSetLoader.java @@ -29,6 +29,8 @@ import android.provider.MediaStore.Files.FileColumns; import com.android.photos.drawables.DataUriThumbnailDrawable; import com.android.photos.shims.LoaderCompatShim; +import java.util.ArrayList; + public class PhotoSetLoader extends CursorLoader implements LoaderCompatShim<Cursor> { public static final String SUPPORTED_OPERATIONS = "supported_operations"; @@ -95,4 +97,19 @@ public class PhotoSetLoader extends CursorLoader implements LoaderCompatShim<Cur public Uri uriForItem(Cursor item) { return null; } + + @Override + public ArrayList<Uri> urisForSubItems(Cursor item) { + return null; + } + + @Override + public void deleteItemWithPath(Object path) { + + } + + @Override + public Object getPathForItem(Cursor item) { + return null; + } } diff --git a/src/com/android/photos/shims/LoaderCompatShim.java b/src/com/android/photos/shims/LoaderCompatShim.java index 9da4436aa..d5bf710de 100644 --- a/src/com/android/photos/shims/LoaderCompatShim.java +++ b/src/com/android/photos/shims/LoaderCompatShim.java @@ -19,8 +19,13 @@ package com.android.photos.shims; import android.graphics.drawable.Drawable; import android.net.Uri; +import java.util.ArrayList; + public interface LoaderCompatShim<T> { Drawable drawableForItem(T item, Drawable recycle); Uri uriForItem(T item); + ArrayList<Uri> urisForSubItems(T item); + void deleteItemWithPath(Object path); + Object getPathForItem(T item); } diff --git a/src/com/android/photos/shims/MediaItemsLoader.java b/src/com/android/photos/shims/MediaItemsLoader.java index fa41c8ec8..d75823404 100644 --- a/src/com/android/photos/shims/MediaItemsLoader.java +++ b/src/com/android/photos/shims/MediaItemsLoader.java @@ -28,12 +28,16 @@ import android.util.SparseArray; import com.android.gallery3d.data.ContentListener; import com.android.gallery3d.data.DataManager; import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; import com.android.gallery3d.data.MediaSet; import com.android.gallery3d.data.MediaSet.ItemConsumer; import com.android.gallery3d.data.MediaSet.SyncListener; +import com.android.gallery3d.data.Path; import com.android.gallery3d.util.Future; import com.android.photos.data.PhotoSetLoader; +import java.util.ArrayList; + /** * Returns all MediaItems in a MediaSet, wrapping them in a cursor to appear * like a PhotoSetLoader @@ -47,6 +51,7 @@ public class MediaItemsLoader extends AsyncTaskLoader<Cursor> implements LoaderC }; private final MediaSet mMediaSet; + private final DataManager mDataManager; private Future<Integer> mSyncTask = null; private ContentListener mObserver = new ContentListener() { @Override @@ -58,14 +63,15 @@ public class MediaItemsLoader extends AsyncTaskLoader<Cursor> implements LoaderC public MediaItemsLoader(Context context) { super(context); - DataManager dm = DataManager.from(context); - String path = dm.getTopSetPath(DataManager.INCLUDE_ALL); - mMediaSet = dm.getMediaSet(path); + mDataManager = DataManager.from(context); + String path = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL); + mMediaSet = mDataManager.getMediaSet(path); } public MediaItemsLoader(Context context, String parentPath) { super(context); - mMediaSet = DataManager.from(getContext()).getMediaSet(parentPath); + mDataManager = DataManager.from(getContext()); + mMediaSet = mDataManager.getMediaSet(parentPath); } @Override @@ -157,4 +163,27 @@ public class MediaItemsLoader extends AsyncTaskLoader<Cursor> implements LoaderC return mi == null ? null : mi.getContentUri(); } + @Override + public ArrayList<Uri> urisForSubItems(Cursor item) { + return null; + } + + @Override + public void deleteItemWithPath(Object path) { + MediaObject o = mDataManager.getMediaObject((Path) path); + if (o != null) { + o.delete(); + } + } + + @Override + public Object getPathForItem(Cursor item) { + int index = item.getInt(PhotoSetLoader.INDEX_ID); + MediaItem mi = mMediaItems.get(index); + if (mi != null) { + return mi.getPath(); + } + return null; + } + } diff --git a/src/com/android/photos/shims/MediaSetLoader.java b/src/com/android/photos/shims/MediaSetLoader.java index 96c7485bb..d200807f9 100644 --- a/src/com/android/photos/shims/MediaSetLoader.java +++ b/src/com/android/photos/shims/MediaSetLoader.java @@ -26,7 +26,9 @@ import android.net.Uri; import com.android.gallery3d.data.ContentListener; import com.android.gallery3d.data.DataManager; import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; import com.android.gallery3d.data.MediaSet.SyncListener; import com.android.gallery3d.util.Future; import com.android.photos.data.AlbumSetLoader; @@ -46,6 +48,7 @@ public class MediaSetLoader extends AsyncTaskLoader<Cursor> implements LoaderCom }; private final MediaSet mMediaSet; + private final DataManager mDataManager; private Future<Integer> mSyncTask = null; private ContentListener mObserver = new ContentListener() { @Override @@ -58,14 +61,15 @@ public class MediaSetLoader extends AsyncTaskLoader<Cursor> implements LoaderCom public MediaSetLoader(Context context) { super(context); - DataManager dm = DataManager.from(context); - String path = dm.getTopSetPath(DataManager.INCLUDE_ALL); - mMediaSet = dm.getMediaSet(path); + mDataManager = DataManager.from(context); + String path = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL); + mMediaSet = mDataManager.getMediaSet(path); } public MediaSetLoader(Context context, String path) { super(context); - mMediaSet = DataManager.from(getContext()).getMediaSet(path); + mDataManager = DataManager.from(getContext()); + mMediaSet = mDataManager.getMediaSet(path); } @Override @@ -111,6 +115,7 @@ public class MediaSetLoader extends AsyncTaskLoader<Cursor> implements LoaderCom row[AlbumSetLoader.INDEX_ID] = i; row[AlbumSetLoader.INDEX_TITLE] = m.getName(); row[AlbumSetLoader.INDEX_COUNT] = m.getMediaItemCount(); + row[AlbumSetLoader.INDEX_SUPPORTED_OPERATIONS] = m.getSupportedOperations(); MediaItem coverItem = m.getCoverMediaItem(); if (coverItem != null) { row[AlbumSetLoader.INDEX_TIMESTAMP] = coverItem.getDateInMs(); @@ -147,4 +152,39 @@ public class MediaSetLoader extends AsyncTaskLoader<Cursor> implements LoaderCom MediaSet ms = mMediaSet.getSubMediaSet(index); return ms == null ? null : ms.getContentUri(); } + + @Override + public ArrayList<Uri> urisForSubItems(Cursor item) { + int index = item.getInt(AlbumSetLoader.INDEX_ID); + MediaSet ms = mMediaSet.getSubMediaSet(index); + if (ms == null) return null; + final ArrayList<Uri> result = new ArrayList<Uri>(); + ms.enumerateMediaItems(new MediaSet.ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + if (item != null) { + result.add(item.getContentUri()); + } + } + }); + return result; + } + + @Override + public void deleteItemWithPath(Object path) { + MediaObject o = mDataManager.getMediaObject((Path) path); + if (o != null) { + o.delete(); + } + } + + @Override + public Object getPathForItem(Cursor item) { + int index = item.getInt(AlbumSetLoader.INDEX_ID); + MediaSet ms = mMediaSet.getSubMediaSet(index); + if (ms != null) { + return ms.getPath(); + } + return null; + } } |