summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:32:20 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:32:20 -0800
commitb64d345c9d51cabce43b5191532a0c185d2a70a5 (patch)
treecb393a35078ffef3dc6d6c0cfa0d9cd349e8cf08 /src
parentbecfb351a5bc43050128f44eb1bcfbcc0c6dcb7a (diff)
downloadLegacyCamera-b64d345c9d51cabce43b5191532a0c185d2a70a5.zip
LegacyCamera-b64d345c9d51cabce43b5191532a0c185d2a70a5.tar.gz
LegacyCamera-b64d345c9d51cabce43b5191532a0c185d2a70a5.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'src')
-rw-r--r--src/com/android/camera/ActionMenuButton.java83
-rw-r--r--src/com/android/camera/Camera.java1861
-rw-r--r--src/com/android/camera/CameraButtonIntentReceiver.java41
-rw-r--r--src/com/android/camera/CameraSettings.java93
-rw-r--r--src/com/android/camera/CameraThread.java95
-rw-r--r--src/com/android/camera/CropImage.java802
-rw-r--r--src/com/android/camera/DrmWallpaper.java35
-rw-r--r--src/com/android/camera/ErrorScreen.java95
-rw-r--r--src/com/android/camera/ExifInterface.java263
-rw-r--r--src/com/android/camera/GalleryPicker.java728
-rw-r--r--src/com/android/camera/GalleryPickerItem.java99
-rw-r--r--src/com/android/camera/GallerySettings.java39
-rw-r--r--src/com/android/camera/HighlightView.java428
-rw-r--r--src/com/android/camera/ImageGallery2.java1848
-rw-r--r--src/com/android/camera/ImageLoader.java342
-rwxr-xr-xsrc/com/android/camera/ImageManager.java4203
-rw-r--r--src/com/android/camera/ImageViewTouchBase.java559
-rw-r--r--src/com/android/camera/MenuHelper.java720
-rw-r--r--src/com/android/camera/MovieView.java255
-rw-r--r--src/com/android/camera/OnScreenHint.java296
-rw-r--r--src/com/android/camera/PhotoGadgetBind.java73
-rw-r--r--src/com/android/camera/PhotoGadgetConfigure.java97
-rw-r--r--src/com/android/camera/PhotoGadgetProvider.java223
-rw-r--r--src/com/android/camera/PickWallpaper.java23
-rw-r--r--src/com/android/camera/SelectedImageGetter.java25
-rw-r--r--src/com/android/camera/ShutterButton.java115
-rw-r--r--src/com/android/camera/SlideShow.java429
-rw-r--r--src/com/android/camera/VideoCamera.java1040
-rw-r--r--src/com/android/camera/VideoPreview.java99
-rw-r--r--src/com/android/camera/ViewImage.java1677
-rw-r--r--src/com/android/camera/Wallpaper.java206
31 files changed, 16892 insertions, 0 deletions
diff --git a/src/com/android/camera/ActionMenuButton.java b/src/com/android/camera/ActionMenuButton.java
new file mode 100644
index 0000000..65e1f0e
--- /dev/null
+++ b/src/com/android/camera/ActionMenuButton.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2008 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;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.text.Layout;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+/**
+ * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
+ * because we want to make the bubble taller than the text and TextView's clip is
+ * too aggressive.
+ */
+public class ActionMenuButton extends TextView {
+ private static final float CORNER_RADIUS = 8.0f;
+ private static final float PADDING_H = 5.0f;
+ private static final float PADDING_V = 1.0f;
+
+ private final RectF mRect = new RectF();
+ private Paint mPaint;
+
+ public ActionMenuButton(Context context) {
+ super(context);
+ init();
+ }
+
+ public ActionMenuButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public ActionMenuButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ setFocusable(true);
+
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaint.setColor(getContext().getResources().getColor(R.color.bubble_dark_background));
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ invalidate();
+ super.drawableStateChanged();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Layout layout = getLayout();
+ final RectF rect = mRect;
+ final int left = getCompoundPaddingLeft();
+ final int top = getExtendedPaddingTop();
+
+ rect.set(left + layout.getLineLeft(0) - PADDING_H,
+ top + layout.getLineTop(0) - PADDING_V,
+ Math.min(left + layout.getLineRight(0) + PADDING_H, mScrollX + mRight - mLeft),
+ top + layout.getLineBottom(0) + PADDING_V);
+ canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, mPaint);
+
+ super.draw(canvas);
+ }
+}
diff --git a/src/com/android/camera/Camera.java b/src/com/android/camera/Camera.java
new file mode 100644
index 0000000..da4c41c
--- /dev/null
+++ b/src/com/android/camera/Camera.java
@@ -0,0 +1,1861 @@
+/*
+ * Copyright (C) 2007 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;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.hardware.Camera.PictureCallback;
+import android.hardware.Camera.Size;
+import android.location.Location;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.text.format.DateFormat;
+import android.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.OrientationEventListener;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+public class Camera extends Activity implements View.OnClickListener,
+ ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback {
+
+ private static final String TAG = "camera";
+
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_TIME_OPERATIONS = DEBUG && false;
+
+ private static final int CROP_MSG = 1;
+ private static final int KEEP = 2;
+ private static final int RESTART_PREVIEW = 3;
+ private static final int CLEAR_SCREEN_DELAY = 4;
+
+ private static final int SCREEN_DELAY = 2 * 60 * 1000;
+ private static final int FOCUS_BEEP_VOLUME = 100;
+
+ public static final int MENU_SWITCH_TO_VIDEO = 0;
+ public static final int MENU_SWITCH_TO_CAMERA = 1;
+ public static final int MENU_FLASH_SETTING = 2;
+ public static final int MENU_FLASH_AUTO = 3;
+ public static final int MENU_FLASH_ON = 4;
+ public static final int MENU_FLASH_OFF = 5;
+ public static final int MENU_SETTINGS = 6;
+ public static final int MENU_GALLERY_PHOTOS = 7;
+ public static final int MENU_GALLERY_VIDEOS = 8;
+ public static final int MENU_SAVE_SELECT_PHOTOS = 30;
+ public static final int MENU_SAVE_NEW_PHOTO = 31;
+ public static final int MENU_SAVE_GALLERY_PHOTO = 34;
+ public static final int MENU_SAVE_GALLERY_VIDEO_PHOTO = 35;
+ public static final int MENU_SAVE_CAMERA_DONE = 36;
+ public static final int MENU_SAVE_CAMERA_VIDEO_DONE = 37;
+
+ private Toast mToast;
+ private OrientationEventListener mOrientationListener;
+ private int mLastOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+ private SharedPreferences mPreferences;
+
+ private static final int IDLE = 1;
+ private static final int SNAPSHOT_IN_PROGRESS = 2;
+ private static final int SNAPSHOT_COMPLETED = 3;
+
+ private int mStatus = IDLE;
+ private static final String sTempCropFilename = "crop-temp";
+
+ private android.hardware.Camera mCameraDevice;
+ private android.hardware.Camera.Parameters mParameters;
+ private VideoPreview mSurfaceView;
+ private SurfaceHolder mSurfaceHolder = null;
+ private View mBlackout = null;
+
+ private int mOriginalViewFinderWidth, mOriginalViewFinderHeight;
+ private int mViewFinderWidth, mViewFinderHeight;
+ private boolean mPreviewing = false;
+
+ private MediaPlayer mClickSound;
+
+ private Capturer mCaptureObject;
+ private ImageCapture mImageCapture = null;
+
+ private boolean mPausing = false;
+
+ private boolean mIsFocusing = false;
+ private boolean mIsFocused = false;
+ private boolean mIsFocusButtonPressed = false;
+ private boolean mCaptureOnFocus = false;
+
+ private static ContentResolver mContentResolver;
+ private boolean mDidRegister = false;
+
+ private ArrayList<MenuItem> mGalleryItems = new ArrayList<MenuItem>();
+
+ private boolean mMenuSelectionMade;
+
+ private ImageView mLastPictureButton;
+ private LayerDrawable mVignette;
+ private Animation mShowLastPictureButtonAnimation = new AlphaAnimation(0F, 1F);
+ private boolean mShouldShowLastPictureButton;
+ private TransitionDrawable mThumbnailTransition;
+ private Drawable[] mThumbnails;
+ private boolean mShouldTransitionThumbnails;
+ private Uri mLastPictureUri;
+ private Bitmap mLastPictureThumb;
+ private LocationManager mLocationManager = null;
+
+ private ShutterButton mShutterButton;
+
+ private Animation mFocusBlinkAnimation;
+ private View mFocusIndicator;
+ private ToneGenerator mFocusToneGenerator;
+
+
+ private ShutterCallback mShutterCallback = new ShutterCallback();
+ private RawPictureCallback mRawPictureCallback = new RawPictureCallback();
+ private AutoFocusCallback mAutoFocusCallback = new AutoFocusCallback();
+ private long mFocusStartTime;
+ private long mFocusCallbackTime;
+ private long mCaptureStartTime;
+ private long mShutterCallbackTime;
+ private long mRawPictureCallbackTime;
+ private int mPicturesRemaining;
+
+ private boolean mKeepAndRestartPreview;
+
+ // mPostCaptureAlert is non-null only if isImageCaptureIntent() is true.
+ private View mPostCaptureAlert;
+
+
+ private Handler mHandler = new MainHandler();
+ private ProgressDialog mSavingProgress;
+
+ private interface Capturer {
+ Uri getLastCaptureUri();
+ void onSnap();
+ void dismissFreezeFrame(boolean keep);
+ void cancelSave();
+ void cancelAutoDismiss();
+ void setDone(boolean wait);
+ }
+
+ private void cancelSavingNotification() {
+ if (mToast != null) {
+ mToast.cancel();
+ mToast = null;
+ }
+ }
+
+ /** This Handler is used to post message back onto the main thread of the application */
+ private class MainHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case KEEP: {
+ keep();
+ if (mSavingProgress != null) {
+ mSavingProgress.cancel();
+ mSavingProgress = null;
+ }
+
+ mKeepAndRestartPreview = true;
+
+ if (msg.obj != null) {
+ mHandler.post((Runnable)msg.obj);
+ }
+ break;
+ }
+
+ case RESTART_PREVIEW: {
+ if (mStatus == SNAPSHOT_IN_PROGRESS) {
+ // We are still in the processing of taking the picture, wait.
+ // This is is strange. Why are we polling?
+ // TODO remove polling
+ mHandler.sendEmptyMessageDelayed(RESTART_PREVIEW, 100);
+ } else if (mStatus == SNAPSHOT_COMPLETED){
+ mCaptureObject.dismissFreezeFrame(true);
+ hidePostCaptureAlert();
+ }
+ break;
+ }
+
+ case CLEAR_SCREEN_DELAY: {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ break;
+ }
+ }
+ }
+ };
+
+ LocationListener [] mLocationListeners = new LocationListener[] {
+ new LocationListener(LocationManager.GPS_PROVIDER),
+ new LocationListener(LocationManager.NETWORK_PROVIDER)
+ };
+
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ // SD card available
+ updateStorageHint(calculatePicturesRemaining());
+ } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED) ||
+ action.equals(Intent.ACTION_MEDIA_CHECKING)) {
+ // SD card unavailable
+ mPicturesRemaining = MenuHelper.NO_STORAGE_ERROR;
+ updateStorageHint(mPicturesRemaining);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+ Toast.makeText(Camera.this, getResources().getString(R.string.wait), 5000);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+ updateStorageHint();
+ }
+ }
+ };
+
+ private class LocationListener implements android.location.LocationListener {
+ Location mLastLocation;
+ boolean mValid = false;
+ String mProvider;
+
+ public LocationListener(String provider) {
+ mProvider = provider;
+ mLastLocation = new Location(mProvider);
+ }
+
+ public void onLocationChanged(Location newLocation) {
+ if (newLocation.getLatitude() == 0.0 && newLocation.getLongitude() == 0.0) {
+ // Hack to filter out 0.0,0.0 locations
+ return;
+ }
+ mLastLocation.set(newLocation);
+ mValid = true;
+ }
+
+ public void onProviderEnabled(String provider) {
+ }
+
+ public void onProviderDisabled(String provider) {
+ mValid = false;
+ }
+
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ if (status == LocationProvider.OUT_OF_SERVICE) {
+ mValid = false;
+ }
+ }
+
+ public Location current() {
+ return mValid ? mLastLocation : null;
+ }
+ };
+
+ private boolean mImageSavingItem = false;
+
+ private final class ShutterCallback implements android.hardware.Camera.ShutterCallback {
+ public void onShutter() {
+ if (DEBUG_TIME_OPERATIONS) {
+ mShutterCallbackTime = System.currentTimeMillis();
+ Log.v(TAG, "Shutter lag was " + (mShutterCallbackTime - mCaptureStartTime) + " ms.");
+ }
+ if (mClickSound != null) {
+ mClickSound.start();
+ }
+ mBlackout.setVisibility(View.VISIBLE);
+
+ Size pictureSize = mParameters.getPictureSize();
+ // Resize the SurfaceView to the aspect-ratio of the still image
+ // and so that we can see the full image that was taken.
+ mSurfaceView.setAspectRatio(pictureSize.width, pictureSize.height);
+ }
+ };
+
+ private final class RawPictureCallback implements PictureCallback {
+ public void onPictureTaken(byte [] rawData, android.hardware.Camera camera) {
+ if (Config.LOGV)
+ Log.v(TAG, "got RawPictureCallback...");
+ mRawPictureCallbackTime = System.currentTimeMillis();
+ if (DEBUG_TIME_OPERATIONS) {
+ Log.v(TAG, (mRawPictureCallbackTime - mShutterCallbackTime) + "ms elapsed between" +
+ " ShutterCallback and RawPictureCallback.");
+ }
+ mBlackout.setVisibility(View.GONE);
+ }
+ };
+
+ private final class JpegPictureCallback implements PictureCallback {
+ Location mLocation;
+
+ public JpegPictureCallback(Location loc) {
+ mLocation = loc;
+ }
+
+ public void onPictureTaken(byte [] jpegData, android.hardware.Camera camera) {
+ if (Config.LOGV)
+ Log.v(TAG, "got JpegPictureCallback...");
+
+ if (DEBUG_TIME_OPERATIONS) {
+ long mJpegPictureCallback = System.currentTimeMillis();
+ Log.v(TAG, (mJpegPictureCallback - mRawPictureCallbackTime) + "ms elapsed between" +
+ " RawPictureCallback and JpegPictureCallback.");
+ }
+
+ mImageCapture.storeImage(jpegData, camera, mLocation);
+
+ mStatus = SNAPSHOT_COMPLETED;
+
+ if (mKeepAndRestartPreview) {
+ long delay = 1500 - (System.currentTimeMillis() - mRawPictureCallbackTime);
+ mHandler.sendEmptyMessageDelayed(RESTART_PREVIEW, Math.max(delay, 0));
+ }
+ }
+ };
+
+ private final class AutoFocusCallback implements android.hardware.Camera.AutoFocusCallback {
+ public void onAutoFocus(boolean focused, android.hardware.Camera camera) {
+ if (DEBUG_TIME_OPERATIONS) {
+ mFocusCallbackTime = System.currentTimeMillis();
+ Log.v(TAG, "Auto focus took " + (mFocusCallbackTime - mFocusStartTime) + " ms.");
+ }
+
+ mIsFocusing = false;
+ mIsFocused = focused;
+ if (focused) {
+ if (mCaptureOnFocus && mCaptureObject != null) {
+ // No need to play the AF sound if we're about to play the shutter sound
+ mCaptureObject.onSnap();
+ clearFocus();
+ } else {
+ ToneGenerator tg = mFocusToneGenerator;
+ if (tg != null)
+ tg.startTone(ToneGenerator.TONE_PROP_BEEP2);
+ }
+ mCaptureOnFocus = false;
+ }
+ updateFocusIndicator();
+ }
+ };
+
+ private class ImageCapture implements Capturer {
+
+ private boolean mCancel = false;
+ private boolean mCapturing = false;
+
+ private Uri mLastContentUri;
+ private ImageManager.IAddImage_cancelable mAddImageCancelable;
+
+ Bitmap mCaptureOnlyBitmap;
+
+ /** These member variables are used for various debug timings */
+ private long mThreadTimeStart;
+ private long mThreadTimeEnd;
+ private long mWallTimeStart;
+ private long mWallTimeEnd;
+
+
+ public ImageCapture() {
+ }
+
+ /**
+ * This method sets whether or not we are capturing a picture. This method must be called
+ * with the ImageCapture.this lock held.
+ */
+ public void setCapturingLocked(boolean capturing) {
+ mCapturing = capturing;
+ }
+
+ /*
+ * Tell the ImageCapture thread to exit when possible.
+ */
+ public void setDone(boolean wait) {
+ }
+
+ /*
+ * Tell the image capture thread to not "dismiss" the current
+ * capture when the current image is stored, etc.
+ */
+ public void cancelAutoDismiss() {
+ }
+
+ public void dismissFreezeFrame(boolean keep) {
+ if (keep) {
+ cancelSavingNotification();
+ } else {
+ Toast.makeText(Camera.this, R.string.camera_tossing, Toast.LENGTH_SHORT).show();
+ }
+
+ if (mStatus == SNAPSHOT_IN_PROGRESS) {
+ // If we are still in the process of taking a picture, then just post a message.
+ mHandler.sendEmptyMessage(RESTART_PREVIEW);
+ } else {
+ restartPreview();
+ }
+ }
+
+ private void startTiming() {
+ mWallTimeStart = SystemClock.elapsedRealtime();
+ mThreadTimeStart = Debug.threadCpuTimeNanos();
+ }
+
+ private void stopTiming() {
+ mThreadTimeEnd = Debug.threadCpuTimeNanos();
+ mWallTimeEnd = SystemClock.elapsedRealtime();
+ }
+
+ private void storeImage(byte[] data, Location loc) {
+ try {
+ if (DEBUG_TIME_OPERATIONS) {
+ startTiming();
+ }
+ long dateTaken = System.currentTimeMillis();
+ String name = createName(dateTaken) + ".jpg";
+ mLastContentUri = ImageManager.instance().addImage(
+ Camera.this,
+ mContentResolver,
+ name,
+ "",
+ dateTaken,
+ // location for the database goes here
+ loc,
+ 0, // the dsp will use the right orientation so don't "double set it"
+ ImageManager.CAMERA_IMAGE_BUCKET_NAME,
+ name);
+
+ if (mLastContentUri == null) {
+ // this means we got an error
+ mCancel = true;
+ }
+ if (!mCancel) {
+ mAddImageCancelable = ImageManager.instance().storeImage(mLastContentUri,
+ Camera.this, mContentResolver, 0, null, data);
+ mAddImageCancelable.get();
+ mAddImageCancelable = null;
+ }
+
+ if (DEBUG_TIME_OPERATIONS) {
+ stopTiming();
+ Log.d(TAG, "Storing image took " + (mWallTimeEnd - mWallTimeStart) + " ms. " +
+ "Thread time was " + ((mThreadTimeEnd - mThreadTimeStart) / 1000000) +
+ " ms.");
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception while compressing image.", ex);
+ }
+ }
+
+ public void storeImage(byte[] data, android.hardware.Camera camera, Location loc) {
+ boolean captureOnly = isImageCaptureIntent();
+
+ if (!captureOnly) {
+ storeImage(data, loc);
+ sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", mLastContentUri));
+ setLastPictureThumb(data, mCaptureObject.getLastCaptureUri());
+ dismissFreezeFrame(true);
+ } else {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 4;
+
+ if (DEBUG_TIME_OPERATIONS) {
+ startTiming();
+ }
+
+ mCaptureOnlyBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
+
+ if (DEBUG_TIME_OPERATIONS) {
+ stopTiming();
+ Log.d(TAG, "Decoded mCaptureOnly bitmap (" + mCaptureOnlyBitmap.getWidth() +
+ "x" + mCaptureOnlyBitmap.getHeight() + " ) in " +
+ (mWallTimeEnd - mWallTimeStart) + " ms. Thread time was " +
+ ((mThreadTimeEnd - mThreadTimeStart) / 1000000) + " ms.");
+ }
+
+ showPostCaptureAlert();
+ cancelAutomaticPreviewRestart();
+ }
+
+ mCapturing = false;
+ if (mPausing) {
+ closeCamera();
+ }
+ }
+
+ /*
+ * Tells the image capture thread to abort the capture of the
+ * current image.
+ */
+ public void cancelSave() {
+ if (!mCapturing) {
+ return;
+ }
+
+ mCancel = true;
+
+ if (mAddImageCancelable != null) {
+ mAddImageCancelable.cancel();
+ }
+ dismissFreezeFrame(false);
+ }
+
+ /*
+ * Initiate the capture of an image.
+ */
+ public void initiate(boolean captureOnly) {
+ if (mCameraDevice == null) {
+ return;
+ }
+
+ mCancel = false;
+ mCapturing = true;
+
+ capture(captureOnly);
+ }
+
+ public Uri getLastCaptureUri() {
+ return mLastContentUri;
+ }
+
+ public Bitmap getLastBitmap() {
+ return mCaptureOnlyBitmap;
+ }
+
+ private void capture(boolean captureOnly) {
+ mPreviewing = false;
+ mCaptureOnlyBitmap = null;
+
+ final int latchedOrientation = ImageManager.roundOrientation(mLastOrientation + 90);
+
+ Boolean recordLocation = mPreferences.getBoolean("pref_camera_recordlocation_key", false);
+ Location loc = recordLocation ? getCurrentLocation() : null;
+ // Quality 75 has visible artifacts, and quality 90 looks great but the files begin to
+ // get large. 85 is a good compromise between the two.
+ mParameters.set("jpeg-quality", 85);
+ mParameters.set("rotation", latchedOrientation);
+
+ mParameters.remove("gps-latitude");
+ mParameters.remove("gps-longitude");
+ mParameters.remove("gps-altitude");
+ mParameters.remove("gps-timestamp");
+
+ if (loc != null) {
+ double lat = loc.getLatitude();
+ double lon = loc.getLongitude();
+ boolean hasLatLon = (lat != 0.0d) || (lon != 0.0d);
+
+ if (hasLatLon) {
+ String latString = String.valueOf(lat);
+ String lonString = String.valueOf(lon);
+ mParameters.set("gps-latitude", latString);
+ mParameters.set("gps-longitude", lonString);
+ if (loc.hasAltitude())
+ mParameters.set("gps-altitude", String.valueOf(loc.getAltitude()));
+ if (loc.getTime() != 0) {
+ // Location.getTime() is UTC in milliseconds.
+ // gps-timestamp is UTC in seconds.
+ long utcTimeSeconds = loc.getTime() / 1000;
+ mParameters.set("gps-timestamp", String.valueOf(utcTimeSeconds));
+ }
+ } else {
+ loc = null;
+ }
+ }
+
+ mCameraDevice.setParameters(mParameters);
+
+ mCameraDevice.takePicture(mShutterCallback, mRawPictureCallback, new JpegPictureCallback(loc));
+ // Prepare the sound to play in shutter callback.
+ if (mClickSound != null) {
+ mClickSound.seekTo(0);
+ }
+ }
+
+ public void onSnap() {
+ if (DEBUG_TIME_OPERATIONS) mCaptureStartTime = System.currentTimeMillis();
+
+ // If we are already in the middle of taking a snapshot then we should just save
+ // the image after we have returned from the camera service.
+ if (mStatus == SNAPSHOT_IN_PROGRESS || mStatus == SNAPSHOT_COMPLETED) {
+ mKeepAndRestartPreview = true;
+ mHandler.sendEmptyMessage(RESTART_PREVIEW);
+ return;
+ }
+
+ // Don't check the filesystem here, we can't afford the latency. Instead, check the
+ // cached value which was calculated when the preview was restarted.
+ if (mPicturesRemaining < 1) {
+ updateStorageHint(mPicturesRemaining);
+ return;
+ }
+
+ mStatus = SNAPSHOT_IN_PROGRESS;
+
+ mKeepAndRestartPreview = true;
+
+ boolean getContentAction = isImageCaptureIntent();
+ if (getContentAction) {
+ mImageCapture.initiate(true);
+ } else {
+ mImageCapture.initiate(false);
+ }
+ }
+ }
+
+ private void setLastPictureThumb(byte[] data, Uri uri) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 16;
+
+ Bitmap lastPictureThumb = BitmapFactory.decodeByteArray(data, 0, data.length, options);
+
+ setLastPictureThumb(lastPictureThumb, uri);
+ }
+
+ private void setLastPictureThumb(Bitmap lastPictureThumb, Uri uri) {
+
+ final int PADDING_WIDTH = 2;
+ final int PADDING_HEIGHT = 2;
+ LayoutParams layoutParams = mLastPictureButton.getLayoutParams();
+ // Make the mini-thumbnail size smaller than the button size so that the image corners
+ // don't peek out from the rounded corners of the frame_thumbnail graphic:
+ final int miniThumbWidth = layoutParams.width - 2 * PADDING_WIDTH;
+ final int miniThumbHeight = layoutParams.height - 2 * PADDING_HEIGHT;
+
+ lastPictureThumb = ImageManager.extractMiniThumb(lastPictureThumb,
+ miniThumbWidth, miniThumbHeight);
+
+ Drawable[] vignetteLayers = new Drawable[2];
+ vignetteLayers[1] = getResources().getDrawable(R.drawable.frame_thumbnail);
+ if (mThumbnails == null) {
+ mThumbnails = new Drawable[2];
+ mThumbnails[1] = new BitmapDrawable(lastPictureThumb);
+ vignetteLayers[0] = mThumbnails[1];
+ } else {
+ mThumbnails[0] = mThumbnails[1];
+ mThumbnails[1] = new BitmapDrawable(lastPictureThumb);
+ mThumbnailTransition = new TransitionDrawable(mThumbnails);
+ mShouldTransitionThumbnails = true;
+ vignetteLayers[0] = mThumbnailTransition;
+ }
+
+ mVignette = new LayerDrawable(vignetteLayers);
+ mVignette.setLayerInset(0, PADDING_WIDTH, PADDING_HEIGHT,
+ PADDING_WIDTH, PADDING_HEIGHT);
+ mLastPictureButton.setImageDrawable(mVignette);
+
+ if (mLastPictureButton.getVisibility() != View.VISIBLE) {
+ mShouldShowLastPictureButton = true;
+ }
+ mLastPictureThumb = lastPictureThumb;
+ mLastPictureUri = uri;
+ }
+
+ static private String createName(long dateTaken) {
+ return DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken).toString();
+ }
+
+ static public Matrix GetDisplayMatrix(Bitmap b, ImageView v) {
+ Matrix m = new Matrix();
+ float bw = (float)b.getWidth();
+ float bh = (float)b.getHeight();
+ float vw = (float)v.getWidth();
+ float vh = (float)v.getHeight();
+ float scale, x, y;
+ if (bw*vh > vw*bh) {
+ scale = vh / bh;
+ x = (vw - scale*bw)*0.5F;
+ y = 0;
+ } else {
+ scale = vw / bw;
+ x = 0;
+ y = (vh - scale*bh)*0.5F;
+ }
+ m.setScale(scale, scale, 0.5F, 0.5F);
+ m.postTranslate(x, y);
+ return m;
+ }
+
+ private void postAfterKeep(final Runnable r) {
+ Resources res = getResources();
+
+ if (mSavingProgress != null) {
+ mSavingProgress = ProgressDialog.show(this, res.getString(R.string.savingImage),
+ res.getString(R.string.wait));
+ }
+
+ Message msg = mHandler.obtainMessage(KEEP);
+ msg.obj = r;
+ msg.sendToTarget();
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // To reduce startup time, we open camera device in another thread.
+ // We make sure the camera is opened at the end of onCreate.
+ Thread openCameraThread = new Thread(new Runnable() {
+ public void run() {
+ mCameraDevice = android.hardware.Camera.open();
+ }
+ });
+ openCameraThread.start();
+
+ // To reduce startup time, we run some service creation code in another thread.
+ // We make sure the services are loaded at the end of onCreate().
+ Thread loadServiceThread = new Thread(new Runnable() {
+ public void run() {
+ mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+ mOrientationListener = new OrientationEventListener(Camera.this) {
+ public void onOrientationChanged(int orientation) {
+ mLastOrientation = orientation;
+ }
+ };
+ }
+ });
+ loadServiceThread.start();
+
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ mContentResolver = getContentResolver();
+
+ Window win = getWindow();
+ win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.camera);
+
+ mSurfaceView = (VideoPreview) findViewById(R.id.camera_preview);
+
+ // don't set mSurfaceHolder here. We have it set ONLY within
+ // surfaceCreated / surfaceDestroyed, other parts of the code
+ // assume that when it is set, the surface is also set.
+ SurfaceHolder holder = mSurfaceView.getHolder();
+ holder.addCallback(this);
+ holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+
+ mBlackout = findViewById(R.id.blackout);
+
+ if (!isImageCaptureIntent()) {
+ mLastPictureButton = (ImageView) findViewById(R.id.last_picture_button);
+ mLastPictureButton.setOnClickListener(this);
+ loadLastThumb();
+ }
+
+ mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
+ mShutterButton.setOnShutterButtonListener(this);
+
+ try {
+ mClickSound = new MediaPlayer();
+ AssetFileDescriptor afd = getResources().openRawResourceFd(R.raw.camera_click);
+
+ mClickSound.setDataSource(afd.getFileDescriptor(),
+ afd.getStartOffset(),
+ afd.getLength());
+
+ if (mClickSound != null) {
+ mClickSound.setAudioStreamType(AudioManager.STREAM_ALARM);
+ mClickSound.prepare();
+ }
+ } catch (Exception ex) {
+ Log.w(TAG, "Couldn't create click sound", ex);
+ }
+
+ mFocusIndicator = findViewById(R.id.focus_indicator);
+ mFocusBlinkAnimation = AnimationUtils.loadAnimation(this, R.anim.auto_focus_blink);
+ mFocusBlinkAnimation.setRepeatCount(Animation.INFINITE);
+ mFocusBlinkAnimation.setRepeatMode(Animation.REVERSE);
+
+ // We load the post_picture_panel layout only if it is needed.
+ if (isImageCaptureIntent()) {
+ ViewGroup cameraView = (ViewGroup)findViewById(R.id.camera);
+ getLayoutInflater().inflate(R.layout.post_picture_panel,
+ cameraView);
+ mPostCaptureAlert = findViewById(R.id.post_picture_panel);
+ }
+
+ // Make sure the services are loaded.
+ try {
+ openCameraThread.join();
+ loadServiceThread.join();
+ } catch (InterruptedException ex) {
+ }
+
+ ImageManager.ensureOSXCompatibleFolder();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+ final boolean storageOK = calculatePicturesRemaining() > 0;
+
+ if (!storageOK) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ updateStorageHint(mPicturesRemaining);
+ }
+ });
+ }
+ }
+ });
+ t.start();
+ }
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.last_picture_button:
+ viewLastImage();
+ break;
+ case R.id.attach:
+ doAttach();
+ break;
+ case R.id.cancel:
+ doCancel();
+ }
+ }
+
+ private void doAttach() {
+ Bitmap bitmap = mImageCapture.getLastBitmap();
+ mCaptureObject.setDone(true);
+
+ String cropValue = null;
+ Uri saveUri = null;
+
+ Bundle myExtras = getIntent().getExtras();
+ if (myExtras != null) {
+ saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+ cropValue = myExtras.getString("crop");
+ }
+
+
+ if (cropValue == null) {
+ /*
+ * First handle the no crop case -- just return the value. If the caller
+ * specifies a "save uri" then write the data to it's stream. Otherwise,
+ * pass back a scaled down version of the bitmap directly in the extras.
+ */
+ if (saveUri != null) {
+ OutputStream outputStream = null;
+ try {
+ outputStream = mContentResolver.openOutputStream(saveUri);
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 75, outputStream);
+ outputStream.close();
+
+ setResult(RESULT_OK);
+ finish();
+ } catch (IOException ex) {
+ //
+ } finally {
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException ex) {
+
+ }
+ }
+ }
+ } else {
+ float scale = .5F;
+ Matrix m = new Matrix();
+ m.setScale(scale, scale);
+
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0,
+ bitmap.getWidth(),
+ bitmap.getHeight(),
+ m, true);
+
+ setResult(RESULT_OK, new Intent("inline-data").putExtra("data", bitmap));
+ finish();
+ }
+ }
+ else {
+ /*
+ * Save the image to a temp file and invoke the cropper
+ */
+ Uri tempUri = null;
+ FileOutputStream tempStream = null;
+ try {
+ File path = getFileStreamPath(sTempCropFilename);
+ path.delete();
+ tempStream = openFileOutput(sTempCropFilename, 0);
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 75, tempStream);
+ tempStream.close();
+ tempUri = Uri.fromFile(path);
+ } catch (FileNotFoundException ex) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ } catch (IOException ex) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ } finally {
+ if (tempStream != null) {
+ try {
+ tempStream.close();
+ } catch (IOException ex) {
+
+ }
+ }
+ }
+
+ Bundle newExtras = new Bundle();
+ if (cropValue.equals("circle"))
+ newExtras.putString("circleCrop", "true");
+ if (saveUri != null)
+ newExtras.putParcelable(MediaStore.EXTRA_OUTPUT, saveUri);
+ else
+ newExtras.putBoolean("return-data", true);
+
+ Intent cropIntent = new Intent();
+ cropIntent.setClass(Camera.this, CropImage.class);
+ cropIntent.setData(tempUri);
+ cropIntent.putExtras(newExtras);
+
+ startActivityForResult(cropIntent, CROP_MSG);
+ }
+ }
+
+ private void doCancel() {
+ setResult(RESULT_CANCELED, new Intent());
+ finish();
+ }
+
+ public void onShutterButtonFocus(ShutterButton button, boolean pressed) {
+ switch (button.getId()) {
+ case R.id.shutter_button:
+ doFocus(pressed);
+ break;
+ }
+ }
+
+ public void onShutterButtonClick(ShutterButton button) {
+ switch (button.getId()) {
+ case R.id.shutter_button:
+ doSnap(false);
+ break;
+ }
+ }
+
+ private void updateStorageHint() {
+ updateStorageHint(MenuHelper.calculatePicturesRemaining());
+ }
+
+ private OnScreenHint mStorageHint;
+
+ private void updateStorageHint(int remaining) {
+ String noStorageText = null;
+
+ if (remaining == MenuHelper.NO_STORAGE_ERROR) {
+ String state = Environment.getExternalStorageState();
+ if (state == Environment.MEDIA_CHECKING) {
+ noStorageText = getString(R.string.preparing_sd);
+ } else {
+ noStorageText = getString(R.string.no_storage);
+ }
+ } else if (remaining < 1) {
+ noStorageText = getString(R.string.not_enough_space);
+ }
+
+ if (noStorageText != null) {
+ if (mStorageHint == null) {
+ mStorageHint = OnScreenHint.makeText(this, noStorageText);
+ } else {
+ mStorageHint.setText(noStorageText);
+ }
+ mStorageHint.show();
+ } else if (mStorageHint != null) {
+ mStorageHint.cancel();
+ mStorageHint = null;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+
+ mPausing = false;
+ mOrientationListener.enable();
+
+ // install an intent filter to receive SD card related events.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_CHECKING);
+ intentFilter.addDataScheme("file");
+ registerReceiver(mReceiver, intentFilter);
+ mDidRegister = true;
+
+ mImageCapture = new ImageCapture();
+
+ restartPreview();
+
+ if (mPreferences.getBoolean("pref_camera_recordlocation_key", false))
+ startReceivingLocationUpdates();
+
+ updateFocusIndicator();
+
+ try {
+ mFocusToneGenerator = new ToneGenerator(AudioManager.STREAM_SYSTEM, FOCUS_BEEP_VOLUME);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Exception caught while creating local tone generator: " + e);
+ mFocusToneGenerator = null;
+ }
+
+ mBlackout.setVisibility(View.GONE);
+ }
+
+ private ImageManager.DataLocation dataLocation() {
+ return ImageManager.DataLocation.EXTERNAL;
+ }
+
+ private static final int BUFSIZE = 4096;
+
+ // Stores the thumbnail and URI of last-picture-taken to SD card, so we can
+ // load it the next time the Camera app starts.
+ private void storeLastThumb() {
+ if (mLastPictureUri != null && mLastPictureThumb != null) {
+ try {
+ FileOutputStream f = new FileOutputStream(ImageManager.getLastThumbPath());
+ try {
+ BufferedOutputStream b = new BufferedOutputStream(f, BUFSIZE);
+ try {
+ DataOutputStream d = new DataOutputStream(b);
+ try {
+ d.writeUTF(mLastPictureUri.toString());
+ mLastPictureThumb.compress(Bitmap.CompressFormat.PNG, 100, d);
+ } finally {
+ d.close();
+ b = null;
+ f = null;
+ }
+ } finally {
+ if (b != null) {
+ b.close();
+ f = null;
+ }
+ }
+ } finally {
+ if (f != null) {
+ f.close();
+ }
+ }
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ // Loads the thumbnail and URI of last-picture-taken from SD card.
+ private void loadLastThumb() {
+ try {
+ FileInputStream f = new FileInputStream(ImageManager.getLastThumbPath());
+ try {
+ BufferedInputStream b = new BufferedInputStream(f, BUFSIZE);
+ try {
+ DataInputStream d = new DataInputStream(b);
+ try {
+ Uri lastUri = Uri.parse(d.readUTF());
+ Bitmap lastThumb = BitmapFactory.decodeStream(d);
+ setLastPictureThumb(lastThumb, lastUri);
+ } finally {
+ d.close();
+ b = null;
+ f = null;
+ }
+ } finally {
+ if (b != null) {
+ b.close();
+ f = null;
+ }
+ }
+ } finally {
+ if (f != null) {
+ f.close();
+ }
+ }
+ } catch (IOException e) {
+ }
+ }
+
+ @Override
+ public void onStop() {
+ keep();
+ stopPreview();
+ closeCamera();
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ super.onStop();
+ }
+
+ @Override
+ protected void onPause() {
+ keep();
+
+ mPausing = true;
+ mOrientationListener.disable();
+
+ stopPreview();
+
+ if (!mImageCapture.mCapturing) {
+ closeCamera();
+ }
+ if (mDidRegister) {
+ unregisterReceiver(mReceiver);
+ mDidRegister = false;
+ }
+ stopReceivingLocationUpdates();
+
+ if (mFocusToneGenerator != null) {
+ mFocusToneGenerator.release();
+ mFocusToneGenerator = null;
+ }
+
+ storeLastThumb();
+ if (mStorageHint != null) {
+ mStorageHint.cancel();
+ mStorageHint = null;
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case CROP_MSG: {
+ Intent intent = new Intent();
+ if (data != null) {
+ Bundle extras = data.getExtras();
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ }
+ setResult(resultCode, intent);
+ finish();
+
+ File path = getFileStreamPath(sTempCropFilename);
+ path.delete();
+
+ break;
+ }
+ }
+ }
+
+ private void autoFocus() {
+ updateFocusIndicator();
+ if (!mIsFocusing) {
+ if (mCameraDevice != null) {
+ mIsFocusing = true;
+ mIsFocused = false;
+ mCameraDevice.autoFocus(mAutoFocusCallback);
+ if (DEBUG_TIME_OPERATIONS) {
+ mFocusStartTime = System.currentTimeMillis();
+ }
+ }
+ }
+ }
+
+ private void clearFocus() {
+ mIsFocusing = false;
+ mIsFocused = false;
+ mIsFocusButtonPressed = false;
+ }
+
+ private void updateFocusIndicator() {
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (mIsFocusing || !mIsFocusButtonPressed) {
+ mFocusIndicator.setVisibility(View.GONE);
+ mFocusIndicator.clearAnimation();
+ } else {
+ if (mIsFocused) {
+ mFocusIndicator.setVisibility(View.VISIBLE);
+ mFocusIndicator.clearAnimation();
+ } else {
+ mFocusIndicator.setVisibility(View.VISIBLE);
+ mFocusIndicator.startAnimation(mFocusBlinkAnimation);
+ }
+ }
+ }
+ });
+ }
+
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (mStatus == SNAPSHOT_IN_PROGRESS) {
+ // ignore backs while we're taking a picture
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_FOCUS:
+ if (event.getRepeatCount() == 0) {
+ doFocus(true);
+ }
+ return true;
+ case KeyEvent.KEYCODE_CAMERA:
+ if (event.getRepeatCount() == 0) {
+ doSnap(false);
+ }
+ return true;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ // If we get a dpad center event without any focused view, move the
+ // focus to the shutter button and press it.
+ if (event.getRepeatCount() == 0) {
+ // Start auto-focus immediately to reduce shutter lag. After the shutter button
+ // gets the focus, doFocus() will be called again but it is fine.
+ doFocus(true);
+ if (mShutterButton.isInTouchMode()) {
+ mShutterButton.requestFocusFromTouch();
+ } else {
+ mShutterButton.requestFocus();
+ }
+ mShutterButton.setPressed(true);
+ }
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_FOCUS:
+ doFocus(false);
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ private void doSnap(boolean needAutofocus) {
+ // The camera operates in focus-priority mode, meaning that we take a picture
+ // when focusing completes, and only if it completes successfully. If the user
+ // has half-pressed the shutter and already locked focus, we can take the photo
+ // right away, otherwise we need to start AF.
+ if (mIsFocused || !mPreviewing) {
+ // doesn't get set until the idler runs
+ if (mCaptureObject != null) {
+ mCaptureObject.onSnap();
+ }
+ clearFocus();
+ updateFocusIndicator();
+ } else {
+ // Half pressing the shutter (i.e. the focus button event) will already have
+ // requested AF for us, so just request capture on focus here. If AF has
+ // already failed, we don't want to trigger it again.
+ mCaptureOnFocus = true;
+ if (needAutofocus && !mIsFocusButtonPressed) {
+ // But we do need to start AF for DPAD_CENTER
+ autoFocus();
+ }
+ }
+ }
+
+ private void doFocus(boolean pressed) {
+ if (pressed) {
+ mIsFocusButtonPressed = true;
+ mCaptureOnFocus = false;
+ if (mPreviewing) {
+ autoFocus();
+ } else if (mCaptureObject != null) {
+ // Save and restart preview
+ mCaptureObject.onSnap();
+ }
+ } else {
+ clearFocus();
+ updateFocusIndicator();
+ }
+ }
+
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ // if we're creating the surface, start the preview as well.
+ boolean preview = holder.isCreating();
+ setViewFinder(w, h, preview);
+ mCaptureObject = mImageCapture;
+ }
+
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceHolder = holder;
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ stopPreview();
+ mSurfaceHolder = null;
+ }
+
+ private void closeCamera() {
+ if (mCameraDevice != null) {
+ mCameraDevice.release();
+ mCameraDevice = null;
+ mPreviewing = false;
+ }
+ }
+
+ private boolean ensureCameraDevice() {
+ if (mCameraDevice == null) {
+ mCameraDevice = android.hardware.Camera.open();
+ }
+ return mCameraDevice != null;
+ }
+
+ private boolean isLastPictureValid() {
+ boolean isValid = true;
+ if (mLastPictureUri == null) return false;
+ try {
+ mContentResolver.openFileDescriptor(mLastPictureUri, "r").close();
+ }
+ catch (Exception ex) {
+ isValid = false;
+ Log.e(TAG, ex.toString());
+ }
+ return isValid;
+ }
+
+ private void updateLastImage() {
+ ImageManager.IImageList list = ImageManager.instance().allImages(
+ this,
+ mContentResolver,
+ dataLocation(),
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_ASCENDING,
+ ImageManager.CAMERA_IMAGE_BUCKET_ID);
+ int count = list.getCount();
+ if (count > 0) {
+ ImageManager.IImage image = list.getImageAt(count-1);
+ mLastPictureUri = image.fullSizeImageUri();
+ Log.v(TAG, "updateLastImage: count="+ count +
+ ", lastPictureUri="+mLastPictureUri);
+ setLastPictureThumb(image.miniThumbBitmap(), mLastPictureUri);
+ } else {
+ mLastPictureUri = null;
+ }
+ list.deactivate();
+ }
+
+ private void restartPreview() {
+ VideoPreview surfaceView = mSurfaceView;
+
+ // make sure the surfaceview fills the whole screen when previewing
+ surfaceView.setAspectRatio(VideoPreview.DONT_CARE);
+ setViewFinder(mOriginalViewFinderWidth, mOriginalViewFinderHeight, true);
+ mStatus = IDLE;
+
+ // Calculate this in advance of each shot so we don't add to shutter latency. It's true that
+ // someone else could write to the SD card in the mean time and fill it, but that could have
+ // happened between the shutter press and saving the JPEG too.
+ // TODO: The best longterm solution is to write a reserve file of maximum JPEG size, always
+ // let the user take a picture, and delete that file if needed to save the new photo.
+ calculatePicturesRemaining();
+
+ if (!isLastPictureValid()) {
+ updateLastImage();
+ }
+
+ if (mShouldShowLastPictureButton) {
+ mShouldShowLastPictureButton = false;
+ mLastPictureButton.setVisibility(View.VISIBLE);
+ Animation a = mShowLastPictureButtonAnimation;
+ a.setDuration(500);
+ mLastPictureButton.setAnimation(a);
+ }
+
+ if (mShouldTransitionThumbnails) {
+ mShouldTransitionThumbnails = false;
+ mThumbnailTransition.startTransition(500);
+ }
+ }
+
+ private void setViewFinder(int w, int h, boolean startPreview) {
+ if (mPausing)
+ return;
+
+ if (mPreviewing &&
+ w == mViewFinderWidth &&
+ h == mViewFinderHeight) {
+ return;
+ }
+
+ if (!ensureCameraDevice())
+ return;
+
+ if (mSurfaceHolder == null)
+ return;
+
+ if (isFinishing())
+ return;
+
+ if (mPausing)
+ return;
+
+ // remember view finder size
+ mViewFinderWidth = w;
+ mViewFinderHeight = h;
+ if (mOriginalViewFinderHeight == 0) {
+ mOriginalViewFinderWidth = w;
+ mOriginalViewFinderHeight = h;
+ }
+
+ if (startPreview == false)
+ return;
+
+ /*
+ * start the preview if we're asked to...
+ */
+
+ // we want to start the preview and we're previewing already,
+ // stop the preview first (this will blank the screen).
+ if (mPreviewing)
+ stopPreview();
+
+ // this blanks the screen if the surface changed, no-op otherwise
+ try {
+ mCameraDevice.setPreviewDisplay(mSurfaceHolder);
+ } catch (IOException exception) {
+ mCameraDevice.release();
+ mCameraDevice = null;
+ // TODO: add more exception handling logic here
+ return;
+ }
+
+ // request the preview size, the hardware may not honor it,
+ // if we depended on it we would have to query the size again
+ mParameters = mCameraDevice.getParameters();
+ mParameters.setPreviewSize(w, h);
+ try {
+ mCameraDevice.setParameters(mParameters);
+ } catch (IllegalArgumentException e) {
+ // Ignore this error, it happens in the simulator.
+ }
+
+
+ final long wallTimeStart = SystemClock.elapsedRealtime();
+ final long threadTimeStart = Debug.threadCpuTimeNanos();
+
+ final Object watchDogSync = new Object();
+ Thread watchDog = new Thread(new Runnable() {
+ public void run() {
+ int next_warning = 1;
+ while (true) {
+ try {
+ synchronized (watchDogSync) {
+ watchDogSync.wait(1000);
+ }
+ } catch (InterruptedException ex) {
+ //
+ }
+ if (mPreviewing)
+ break;
+
+ int delay = (int) (SystemClock.elapsedRealtime() - wallTimeStart) / 1000;
+ if (delay >= next_warning) {
+ if (delay < 120) {
+ Log.e(TAG, "preview hasn't started yet in " + delay + " seconds");
+ } else {
+ Log.e(TAG, "preview hasn't started yet in " + (delay / 60) + " minutes");
+ }
+ if (next_warning < 60) {
+ next_warning <<= 1;
+ if (next_warning == 16) {
+ next_warning = 15;
+ }
+ } else {
+ next_warning += 60;
+ }
+ }
+ }
+ }
+ });
+
+ watchDog.start();
+
+ if (Config.LOGV)
+ Log.v(TAG, "calling mCameraDevice.startPreview");
+ try {
+ mCameraDevice.startPreview();
+ } catch (Throwable e) {
+ // TODO: change Throwable to IOException once android.hardware.Camera.startPreview
+ // properly declares that it throws IOException.
+ }
+ mPreviewing = true;
+
+ synchronized (watchDogSync) {
+ watchDogSync.notify();
+ }
+
+ long threadTimeEnd = Debug.threadCpuTimeNanos();
+ long wallTimeEnd = SystemClock.elapsedRealtime();
+ if ((wallTimeEnd - wallTimeStart) > 3000) {
+ Log.w(TAG, "startPreview() to " + (wallTimeEnd - wallTimeStart) + " ms. Thread time was"
+ + (threadTimeEnd - threadTimeStart) / 1000000 + " ms.");
+ }
+ }
+
+ private void stopPreview() {
+ if (mCameraDevice != null && mPreviewing) {
+ mCameraDevice.stopPreview();
+ }
+ mPreviewing = false;
+ }
+
+ void gotoGallery() {
+ MenuHelper.gotoCameraImageGallery(this);
+ }
+
+ private void viewLastImage() {
+ Uri targetUri = mLastPictureUri;
+ if (targetUri != null && isLastPictureValid()) {
+ targetUri = targetUri.buildUpon().
+ appendQueryParameter("bucketId", ImageManager.CAMERA_IMAGE_BUCKET_ID).build();
+ Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
+ intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
+ ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ intent.putExtra(MediaStore.EXTRA_FULL_SCREEN, true);
+ intent.putExtra(MediaStore.EXTRA_SHOW_ACTION_ICONS, true);
+ intent.putExtra("com.android.camera.ReviewMode", true);
+
+ try {
+ startActivity(intent);
+ } catch (android.content.ActivityNotFoundException ex) {
+ // ignore.
+ }
+ } else {
+ Log.e(TAG, "Can't view last image.");
+ }
+ }
+
+ void keep() {
+ cancelSavingNotification();
+ if (mCaptureObject != null) {
+ mCaptureObject.dismissFreezeFrame(true);
+ }
+ };
+
+ void toss() {
+ cancelSavingNotification();
+ if (mCaptureObject != null) {
+ mCaptureObject.cancelSave();
+ }
+ };
+
+ private ImageManager.IImage getImageForURI(Uri uri) {
+ ImageManager.IImageList list = ImageManager.instance().allImages(
+ this,
+ mContentResolver,
+ dataLocation(),
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_ASCENDING);
+ ImageManager.IImage image = list.getImageForUri(uri);
+ list.deactivate();
+ return image;
+ }
+
+
+ private void startReceivingLocationUpdates() {
+ if (mLocationManager != null) {
+ try {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER,
+ 1000,
+ 0F,
+ mLocationListeners[1]);
+ } catch (java.lang.SecurityException ex) {
+ // ok
+ } catch (IllegalArgumentException ex) {
+ if (Config.LOGD) {
+ Log.d(TAG, "provider does not exist " + ex.getMessage());
+ }
+ }
+ try {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ 1000,
+ 0F,
+ mLocationListeners[0]);
+ } catch (java.lang.SecurityException ex) {
+ // ok
+ } catch (IllegalArgumentException ex) {
+ if (Config.LOGD) {
+ Log.d(TAG, "provider does not exist " + ex.getMessage());
+ }
+ }
+ }
+ }
+
+ private void stopReceivingLocationUpdates() {
+ if (mLocationManager != null) {
+ for (int i = 0; i < mLocationListeners.length; i++) {
+ try {
+ mLocationManager.removeUpdates(mLocationListeners[i]);
+ } catch (Exception ex) {
+ // ok
+ }
+ }
+ }
+ }
+
+ private Location getCurrentLocation() {
+ Location l = null;
+
+ // go in best to worst order
+ for (int i = 0; i < mLocationListeners.length; i++) {
+ l = mLocationListeners[i].current();
+ if (l != null)
+ break;
+ }
+
+ return l;
+ }
+
+ @Override
+ public void onOptionsMenuClosed(Menu menu) {
+ super.onOptionsMenuClosed(menu);
+ if (mImageSavingItem && !mMenuSelectionMade) {
+ // save the image if we presented the "advanced" menu
+ // which happens if "menu" is pressed while in
+ // SNAPSHOT_IN_PROGRESS or SNAPSHOT_COMPLETED modes
+ keep();
+ mHandler.sendEmptyMessage(RESTART_PREVIEW);
+ }
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL) {
+ if (mStatus == SNAPSHOT_IN_PROGRESS) {
+ cancelAutomaticPreviewRestart();
+ mMenuSelectionMade = false;
+ }
+ }
+ return super.onMenuOpened(featureId, menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ mMenuSelectionMade = false;
+
+ for (int i = 1; i <= MenuHelper.MENU_ITEM_MAX; i++) {
+ if (i != MenuHelper.GENERIC_ITEM) {
+ menu.setGroupVisible(i, false);
+ }
+ }
+
+ if (mStatus == SNAPSHOT_IN_PROGRESS || mStatus == SNAPSHOT_COMPLETED) {
+ menu.setGroupVisible(MenuHelper.IMAGE_SAVING_ITEM, true);
+ mImageSavingItem = true;
+ } else {
+ menu.setGroupVisible(MenuHelper.IMAGE_MODE_ITEM, true);
+ mImageSavingItem = false;
+ }
+
+ if (mCaptureObject != null)
+ mCaptureObject.cancelAutoDismiss();
+
+ return true;
+ }
+
+ private void cancelAutomaticPreviewRestart() {
+ mKeepAndRestartPreview = false;
+ mHandler.removeMessages(RESTART_PREVIEW);
+ }
+
+ private boolean isImageCaptureIntent() {
+ String action = getIntent().getAction();
+ return (MediaStore.ACTION_IMAGE_CAPTURE.equals(action));
+ }
+
+ private void showPostCaptureAlert() {
+ if (isImageCaptureIntent()) {
+ mPostCaptureAlert.setVisibility(View.VISIBLE);
+ int[] pickIds = {R.id.attach, R.id.cancel};
+ for(int id : pickIds) {
+ View view = mPostCaptureAlert.findViewById(id);
+ view.setOnClickListener(this);
+ Animation animation = new AlphaAnimation(0F, 1F);
+ animation.setDuration(500);
+ view.setAnimation(animation);
+ }
+ }
+ }
+
+ private void hidePostCaptureAlert() {
+ if (isImageCaptureIntent()) {
+ mPostCaptureAlert.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ if (isImageCaptureIntent()) {
+ // No options menu for attach mode.
+ return false;
+ } else {
+ addBaseMenuItems(menu);
+ MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_ROTATE_MENU,
+ true,
+ Camera.this,
+ mHandler,
+
+ // Handler for deletion
+ new Runnable() {
+ public void run() {
+ if (mCaptureObject != null) {
+ mCaptureObject.cancelSave();
+ Uri uri = mCaptureObject.getLastCaptureUri();
+ if (uri != null) {
+ mContentResolver.delete(uri, null, null);
+ }
+ }
+ }
+ },
+ new MenuHelper.MenuInvoker() {
+ public void run(final MenuHelper.MenuCallback cb) {
+ mMenuSelectionMade = true;
+ postAfterKeep(new Runnable() {
+ public void run() {
+ cb.run(mSelectedImageGetter.getCurrentImageUri(), mSelectedImageGetter.getCurrentImage());
+ if (mCaptureObject != null)
+ mCaptureObject.dismissFreezeFrame(true);
+ }
+ });
+ }
+ });
+
+ MenuItem gallery = menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_GALLERY_PHOTO, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ postAfterKeep(new Runnable() {
+ public void run() {
+ gotoGallery();
+ }
+ });
+ return true;
+ }
+ });
+ gallery.setIcon(android.R.drawable.ic_menu_gallery);
+ }
+ return true;
+ }
+
+ SelectedImageGetter mSelectedImageGetter =
+ new SelectedImageGetter() {
+ public ImageManager.IImage getCurrentImage() {
+ return getImageForURI(getCurrentImageUri());
+ }
+ public Uri getCurrentImageUri() {
+ keep();
+ return mCaptureObject.getLastCaptureUri();
+ }
+ };
+
+ private int calculatePicturesRemaining() {
+ mPicturesRemaining = MenuHelper.calculatePicturesRemaining();
+ return mPicturesRemaining;
+ }
+
+ private void addBaseMenuItems(Menu menu) {
+ MenuHelper.addSwitchModeMenuItem(menu, this, true);
+ {
+ MenuItem gallery = menu.add(MenuHelper.IMAGE_MODE_ITEM, MENU_GALLERY_PHOTOS, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ gotoGallery();
+ return true;
+ }
+ });
+ gallery.setIcon(android.R.drawable.ic_menu_gallery);
+ mGalleryItems.add(gallery);
+ }
+ {
+ MenuItem gallery = menu.add(MenuHelper.VIDEO_MODE_ITEM, MENU_GALLERY_VIDEOS, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ gotoGallery();
+ return true;
+ }
+ });
+ gallery.setIcon(android.R.drawable.ic_menu_gallery);
+ mGalleryItems.add(gallery);
+ }
+
+ MenuItem item = menu.add(MenuHelper.GENERIC_ITEM, MENU_SETTINGS, 0, R.string.settings).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent intent = new Intent();
+ intent.setClass(Camera.this, CameraSettings.class);
+ startActivity(intent);
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+ }
+}
+
diff --git a/src/com/android/camera/CameraButtonIntentReceiver.java b/src/com/android/camera/CameraButtonIntentReceiver.java
new file mode 100644
index 0000000..5e4d3c3
--- /dev/null
+++ b/src/com/android/camera/CameraButtonIntentReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.view.KeyEvent;
+
+public class CameraButtonIntentReceiver extends BroadcastReceiver {
+ public CameraButtonIntentReceiver() {
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+
+ if (event == null) {
+ return;
+ }
+
+ Intent i = new Intent(Intent.ACTION_MAIN);
+ i.setClass(context, Camera.class);
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(i);
+ }
+}
diff --git a/src/com/android/camera/CameraSettings.java b/src/com/android/camera/CameraSettings.java
new file mode 100644
index 0000000..0145d64
--- /dev/null
+++ b/src/com/android/camera/CameraSettings.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.PreferenceActivity;
+
+/**
+ * CameraSettings
+ */
+public class CameraSettings extends PreferenceActivity
+ implements OnSharedPreferenceChangeListener
+{
+ public static final String KEY_VIDEO_QUALITY = "pref_camera_videoquality_key";
+ public static final boolean DEFAULT_VIDEO_QUALITY_VALUE = true;
+
+ private ListPreference mVideoQuality;
+
+ public CameraSettings()
+ {
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.camera_preferences);
+
+ initUI();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ updateVideoQuality();
+ }
+
+ private void initUI() {
+ mVideoQuality = (ListPreference) findPreference(KEY_VIDEO_QUALITY);
+ getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
+ }
+
+ private void updateVideoQuality() {
+ boolean vidQualityValue = getBooleanPreference(mVideoQuality, DEFAULT_VIDEO_QUALITY_VALUE);
+ int vidQualityIndex = vidQualityValue ? 1 : 0;
+ String[] vidQualities =
+ getResources().getStringArray(R.array.pref_camera_videoquality_entries);
+ String vidQuality = vidQualities[vidQualityIndex];
+ mVideoQuality.setSummary(vidQuality);
+ }
+
+ private static int getIntPreference(ListPreference preference, int defaultValue) {
+ String s = preference.getValue();
+ int result = defaultValue;
+ try {
+ result = Integer.parseInt(s);
+ } catch (NumberFormatException e) {
+ // Ignore, result is already the default value.
+ }
+ return result;
+ }
+
+ private boolean getBooleanPreference(ListPreference preference, boolean defaultValue) {
+ return getIntPreference(preference, defaultValue ? 1 : 0) != 0;
+ }
+
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ if (key.equals(KEY_VIDEO_QUALITY)) {
+ updateVideoQuality();
+ }
+
+ }
+}
+
diff --git a/src/com/android/camera/CameraThread.java b/src/com/android/camera/CameraThread.java
new file mode 100644
index 0000000..ba888bf
--- /dev/null
+++ b/src/com/android/camera/CameraThread.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.os.Process;
+
+public class CameraThread {
+ private Thread mThread;
+ private int mTid;
+ private boolean mTidSet;
+ private boolean mFinished;
+
+ synchronized private void setTid(int tid) {
+ mTid = tid;
+ mTidSet = true;
+ CameraThread.this.notifyAll();
+ }
+
+ synchronized private void setFinished() {
+ mFinished = true;
+ }
+
+ public CameraThread(final Runnable r) {
+ Runnable wrapper = new Runnable() {
+ public void run() {
+ setTid(Process.myTid());
+ try {
+ r.run();
+ } finally {
+ setFinished();
+ }
+ }
+ };
+
+ mThread = new Thread(wrapper);
+ }
+
+ synchronized public void start() {
+ mThread.start();
+ }
+
+ synchronized public void setName(String name) {
+ mThread.setName(name);
+ }
+
+ public void join() {
+ try {
+ mThread.join();
+ } catch (InterruptedException ex) {
+ // ok?
+ }
+ }
+
+ public long getId() {
+ return mThread.getId();
+ }
+
+ public Thread realThread() {
+ return mThread;
+ }
+
+ synchronized public void setPriority(int androidOsPriority) {
+ while (!mTidSet) {
+ try {
+ CameraThread.this.wait();
+ } catch (InterruptedException ex) {
+ // ok, try again
+ }
+ }
+ if (!mFinished)
+ Process.setThreadPriority(mTid, androidOsPriority);
+ }
+
+ synchronized public void toBackground() {
+ setPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ }
+
+ synchronized public void toForeground() {
+ setPriority(Process.THREAD_PRIORITY_FOREGROUND);
+ }
+}
diff --git a/src/com/android/camera/CropImage.java b/src/com/android/camera/CropImage.java
new file mode 100644
index 0000000..cefaf83
--- /dev/null
+++ b/src/com/android/camera/CropImage.java
@@ -0,0 +1,802 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.media.FaceDetector;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+
+public class CropImage extends Activity {
+ private static final String TAG = "CropImage";
+ private ProgressDialog mFaceDetectionDialog = null;
+ private ProgressDialog mSavingProgressDialog = null;
+ private ImageManager.IImageList mAllImages;
+ private Bitmap.CompressFormat mSaveFormat = Bitmap.CompressFormat.JPEG; // only used with mSaveUri
+ private Uri mSaveUri = null;
+ private int mAspectX, mAspectY;
+ private int mOutputX, mOutputY;
+ private boolean mDoFaceDetection = true;
+ private boolean mCircleCrop = false;
+ private boolean mWaitingToPick;
+ private boolean mScale;
+ private boolean mSaving;
+ private boolean mScaleUp = true;
+
+ CropImageView mImageView;
+ ContentResolver mContentResolver;
+
+ Bitmap mBitmap;
+ Bitmap mCroppedImage;
+ HighlightView mCrop;
+
+ ImageManager.IImage mImage;
+
+ public CropImage() {
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ static public class CropImageView extends ImageViewTouchBase {
+ ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>();
+ HighlightView mMotionHighlightView = null;
+ float mLastX, mLastY;
+ int mMotionEdge;
+
+ public CropImageView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected boolean doesScrolling() {
+ return false;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mBitmapDisplayed != null) {
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ if (hv.mIsFocused) {
+ centerBasedOnHighlightView(hv);
+ }
+ }
+ }
+ }
+
+ public CropImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ protected void zoomTo(float scale, float centerX, float centerY) {
+ super.zoomTo(scale, centerX, centerY);
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+ protected void zoomIn() {
+ super.zoomIn();
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+ protected void zoomOut() {
+ super.zoomOut();
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+
+ @Override
+ protected boolean usePerfectFitBitmap() {
+ return false;
+ }
+
+ @Override
+ protected void postTranslate(float deltaX, float deltaY) {
+ super.postTranslate(deltaX, deltaY);
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ hv.mMatrix.postTranslate(deltaX, deltaY);
+ hv.invalidate();
+ }
+ }
+
+ private void recomputeFocus(MotionEvent event) {
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ hv.setFocus(false);
+ hv.invalidate();
+ }
+
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ int edge = hv.getHit(event.getX(), event.getY());
+ if (edge != HighlightView.GROW_NONE) {
+ if (!hv.hasFocus()) {
+ hv.setFocus(true);
+ hv.invalidate();
+ }
+ break;
+ }
+ }
+ invalidate();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ CropImage cropImage = (CropImage)mContext;
+ if (cropImage.mSaving)
+ return false;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (cropImage.mWaitingToPick) {
+ recomputeFocus(event);
+ } else {
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ int edge = hv.getHit(event.getX(), event.getY());
+ if (edge != HighlightView.GROW_NONE) {
+ mMotionEdge = edge;
+ mMotionHighlightView = hv;
+ mLastX = event.getX();
+ mLastY = event.getY();
+ mMotionHighlightView.setMode(edge == HighlightView.MOVE
+ ? HighlightView.ModifyMode.Move
+ : HighlightView.ModifyMode.Grow);
+ break;
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (cropImage.mWaitingToPick) {
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ if (hv.hasFocus()) {
+ cropImage.mCrop = hv;
+ for (int j = 0; j < mHighlightViews.size(); j++) {
+ if (j == i)
+ continue;
+ mHighlightViews.get(j).setHidden(true);
+ }
+ centerBasedOnHighlightView(hv);
+ ((CropImage)mContext).mWaitingToPick = false;
+ return true;
+ }
+ }
+ } else if (mMotionHighlightView != null) {
+ centerBasedOnHighlightView(mMotionHighlightView);
+ mMotionHighlightView.setMode(HighlightView.ModifyMode.None);
+ }
+ mMotionHighlightView = null;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (cropImage.mWaitingToPick) {
+ recomputeFocus(event);
+ } else if (mMotionHighlightView != null) {
+ mMotionHighlightView.handleMotion(mMotionEdge, event.getX()-mLastX, event.getY()-mLastY);
+ mLastX = event.getX();
+ mLastY = event.getY();
+
+ if (true) {
+ // This section of code is optional. It has some user
+ // benefit in that moving the crop rectangle against
+ // the edge of the screen causes scrolling but it means
+ // that the crop rectangle is no longer fixed under
+ // the user's finger.
+ ensureVisible(mMotionHighlightView);
+ }
+ }
+ break;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_UP:
+ center(true, true, true);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // if we're not zoomed then there's no point in even allowing
+ // the user to move the image around. This call to center
+ // puts it back to the normalized location (with false meaning
+ // don't animate).
+ if (getScale() == 1F)
+ center(true, true, false);
+ break;
+ }
+
+ return true;
+ }
+
+ private void ensureVisible(HighlightView hv) {
+ Rect r = hv.mDrawRect;
+
+ int panDeltaX1 = Math.max(0, mLeft - r.left);
+ int panDeltaX2 = Math.min(0, mRight - r.right);
+
+ int panDeltaY1 = Math.max(0, mTop - r.top);
+ int panDeltaY2 = Math.min(0, mBottom - r.bottom);
+
+ int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
+ int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;
+
+ if (panDeltaX != 0 || panDeltaY != 0)
+ panBy(panDeltaX, panDeltaY);
+ }
+
+ private void centerBasedOnHighlightView(HighlightView hv) {
+ Rect drawRect = hv.mDrawRect;
+
+ float width = drawRect.width();
+ float height = drawRect.height();
+
+ float thisWidth = getWidth();
+ float thisHeight = getHeight();
+
+ float z1 = thisWidth / width * .6F;
+ float z2 = thisHeight / height * .6F;
+
+ float zoom = Math.min(z1, z2);
+ zoom = zoom * this.getScale();
+ zoom = Math.max(1F, zoom);
+
+ if ((Math.abs(zoom - getScale()) / zoom) > .1) {
+ float [] coordinates = new float[] { hv.mCropRect.centerX(), hv.mCropRect.centerY() };
+ getImageMatrix().mapPoints(coordinates);
+ zoomTo(zoom, coordinates[0], coordinates[1], 300F);
+ }
+
+ ensureVisible(hv);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ mHighlightViews.get(i).draw(canvas);
+ }
+ }
+
+ public HighlightView get(int i) {
+ return mHighlightViews.get(i);
+ }
+
+ public int size() {
+ return mHighlightViews.size();
+ }
+
+ public void add(HighlightView hv) {
+ mHighlightViews.add(hv);
+ invalidate();
+ }
+ }
+
+ private void fillCanvas(int width, int height, Canvas c) {
+ Paint paint = new Paint();
+ paint.setColor(0x00000000); // pure alpha
+ paint.setStyle(android.graphics.Paint.Style.FILL);
+ paint.setAntiAlias(true);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ c.drawRect(0F, 0F, width, height, paint);
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ mContentResolver = getContentResolver();
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.cropimage);
+
+ mImageView = (CropImageView) findViewById(R.id.image);
+
+ MenuHelper.showStorageToast(this);
+
+ try {
+ android.content.Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ if (Config.LOGV)
+ Log.v(TAG, "extras are " + extras);
+ if (extras != null) {
+ for (String s: extras.keySet()) {
+ if (Config.LOGV)
+ Log.v(TAG, "" + s + " >>> " + extras.get(s));
+ }
+ if (extras.getString("circleCrop") != null) {
+ mCircleCrop = true;
+ mAspectX = 1;
+ mAspectY = 1;
+ }
+ mSaveUri = (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT);
+ if (mSaveUri != null) {
+ String compressFormatString = extras.getString("outputFormat");
+ if (compressFormatString != null)
+ mSaveFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
+ }
+ mBitmap = (Bitmap) extras.getParcelable("data");
+ mAspectX = extras.getInt("aspectX");
+ mAspectY = extras.getInt("aspectY");
+ mOutputX = extras.getInt("outputX");
+ mOutputY = extras.getInt("outputY");
+ mScale = extras.getBoolean("scale", true);
+ mScaleUp = extras.getBoolean("scaleUpIfNeeded", true);
+ mDoFaceDetection = extras.containsKey("noFaceDetection") ? !extras.getBoolean("noFaceDetection") : true;
+ }
+
+ if (mBitmap == null) {
+ Uri target = intent.getData();
+ mAllImages = ImageManager.makeImageList(target, CropImage.this, ImageManager.SORT_ASCENDING);
+ mImage = mAllImages.getImageForUri(target);
+ if(mImage != null) {
+ // don't read in really large bitmaps. max out at 1000.
+ // TODO when saving the resulting bitmap use the decode/crop/encode
+ // api so we don't lose any resolution
+ mBitmap = mImage.thumbBitmap();
+ if (Config.LOGV)
+ Log.v(TAG, "thumbBitmap returned " + mBitmap);
+ }
+ }
+
+ if (mBitmap == null) {
+ finish();
+ return;
+ }
+
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ if (isFinishing()) {
+ return;
+ }
+ mFaceDetectionDialog = ProgressDialog.show(CropImage.this,
+ null,
+ getResources().getString(R.string.runningFaceDetection),
+ true, false);
+ mImageView.setImageBitmapResetBase(mBitmap, true, true);
+ if (mImageView.getScale() == 1F)
+ mImageView.center(true, true, false);
+
+ new Thread(new Runnable() {
+ public void run() {
+ final Bitmap b = mImage != null ? mImage.fullSizeBitmap(500) : mBitmap;
+ if (Config.LOGV)
+ Log.v(TAG, "back from mImage.fullSizeBitmap(500) with bitmap of size " + b.getWidth() + " / " + b.getHeight());
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (b != mBitmap && b != null) {
+ mBitmap = b;
+ mImageView.setImageBitmapResetBase(b, true, false);
+ }
+ if (mImageView.getScale() == 1F)
+ mImageView.center(true, true, false);
+
+ new Thread(mRunFaceDetection).start();
+ }
+ });
+ }
+ }).start();
+ }}, 100);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to load bitmap", e);
+ finish();
+ }
+
+ findViewById(R.id.discard).setOnClickListener(new android.view.View.OnClickListener() {
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+
+ findViewById(R.id.save).setOnClickListener(new android.view.View.OnClickListener() {
+ public void onClick(View v) {
+ // TODO this code needs to change to use the decode/crop/encode single
+ // step api so that we don't require that the whole (possibly large) bitmap
+ // doesn't have to be read into memory
+ mSaving = true;
+ if (mCroppedImage == null) {
+ if (mCrop == null) {
+ if (Config.LOGV)
+ Log.v(TAG, "no cropped image...");
+ return;
+ }
+
+ Rect r = mCrop.getCropRect();
+
+ int width = r.width();
+ int height = r.height();
+
+ // if we're circle cropping we'll want alpha which is the third param here
+ mCroppedImage = Bitmap.createBitmap(width, height,
+ mCircleCrop ?
+ Bitmap.Config.ARGB_8888 :
+ Bitmap.Config.RGB_565);
+ Canvas c1 = new Canvas(mCroppedImage);
+ c1.drawBitmap(mBitmap, r, new Rect(0, 0, width, height), null);
+
+ if (mCircleCrop) {
+ // OK, so what's all this about?
+ // Bitmaps are inherently rectangular but we want to return something
+ // that's basically a circle. So we fill in the area around the circle
+ // with alpha. Note the all important PortDuff.Mode.CLEAR.
+ Canvas c = new Canvas (mCroppedImage);
+ android.graphics.Path p = new android.graphics.Path();
+ p.addCircle(width/2F, height/2F, width/2F, android.graphics.Path.Direction.CW);
+ c.clipPath(p, Region.Op.DIFFERENCE);
+
+ fillCanvas(width, height, c);
+ }
+ }
+
+ /* If the output is required to a specific size then scale or fill */
+ if (mOutputX != 0 && mOutputY != 0) {
+
+ if (mScale) {
+
+ /* Scale the image to the required dimensions */
+ mCroppedImage = ImageLoader.transform(new Matrix(),
+ mCroppedImage, mOutputX, mOutputY, mScaleUp);
+ } else {
+
+ /* Don't scale the image crop it to the size requested.
+ * Create an new image with the cropped image in the center and
+ * the extra space filled.
+ */
+
+ /* Don't scale the image but instead fill it so it's the required dimension */
+ Bitmap b = Bitmap.createBitmap(mOutputX, mOutputY, Bitmap.Config.RGB_565);
+ Canvas c1 = new Canvas(b);
+
+ /* Draw the cropped bitmap in the center */
+ Rect r = mCrop.getCropRect();
+ int left = (mOutputX / 2) - (r.width() / 2);
+ int top = (mOutputY / 2) - (r.width() / 2);
+ c1.drawBitmap(mBitmap, r, new Rect(left, top, left
+ + r.width(), top + r.height()), null);
+
+ /* Set the cropped bitmap as the new bitmap */
+ mCroppedImage = b;
+ }
+ }
+
+ Bundle myExtras = getIntent().getExtras();
+ if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean("return-data"))) {
+ Bundle extras = new Bundle();
+ extras.putParcelable("data", mCroppedImage);
+ setResult(RESULT_OK,
+ (new Intent()).setAction("inline-data").putExtras(extras));
+ finish();
+ } else {
+ if (!isFinishing()) {
+ mSavingProgressDialog = ProgressDialog.show(CropImage.this,
+ null,
+ getResources().getString(R.string.savingImage),
+ true, true);
+ }
+ Runnable r = new Runnable() {
+ public void run() {
+ if (mSaveUri != null) {
+ OutputStream outputStream = null;
+ try {
+ String scheme = mSaveUri.getScheme();
+ if (scheme.equals("file")) {
+ outputStream = new FileOutputStream(mSaveUri.toString().substring(scheme.length()+":/".length()));
+ } else {
+ outputStream = mContentResolver.openOutputStream(mSaveUri);
+ }
+ if (outputStream != null)
+ mCroppedImage.compress(mSaveFormat, 75, outputStream);
+
+ } catch (IOException ex) {
+ if (Config.LOGV)
+ Log.v(TAG, "got IOException " + ex);
+ } finally {
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException ex) {
+
+ }
+ }
+ }
+ Bundle extras = new Bundle();
+ setResult(RESULT_OK,
+ (new Intent())
+ .setAction(mSaveUri.toString())
+ .putExtras(extras));
+ } else {
+ Bundle extras = new Bundle();
+ extras.putString("rect", mCrop.getCropRect().toString());
+
+ // here we decide whether to create a new image or
+ // modify the existing image
+ if (false) {
+ /*
+ // this is the "modify" case
+ ImageManager.IGetBoolean_cancelable cancelable =
+ mImage.saveImageContents(mCroppedImage, null, null, null, mImage.getDateTaken(), 0, false);
+ boolean didSave = cancelable.get();
+ extras.putString("thumb1uri", mImage.thumbUri().toString());
+ setResult(RESULT_OK,
+ (new Intent()).setAction(mImage.fullSizeImageUri().toString())
+ .putExtras(extras));
+ */
+ } else {
+ // this is the "new image" case
+ java.io.File oldPath = new java.io.File(mImage.getDataPath());
+ java.io.File directory = new java.io.File(oldPath.getParent());
+
+ int x = 0;
+ String fileName = oldPath.getName();
+ fileName = fileName.substring(0, fileName.lastIndexOf("."));
+
+ while (true) {
+ x += 1;
+ String candidate = directory.toString() + "/" + fileName + "-" + x + ".jpg";
+ if (Config.LOGV)
+ Log.v(TAG, "candidate is " + candidate);
+ boolean exists = (new java.io.File(candidate)).exists();
+ if (!exists)
+ break;
+ }
+
+ try {
+ Uri newUri = ImageManager.instance().addImage(
+ CropImage.this,
+ getContentResolver(),
+ mImage.getTitle(),
+ mImage.getDescription(),
+ mImage.getDateTaken(),
+ null, // TODO this null is going to cause us to lose the location (gps)
+ 0, // TODO this is going to cause the orientation to reset
+ directory.toString(),
+ fileName + "-" + x + ".jpg");
+
+ ImageManager.IAddImage_cancelable cancelable = ImageManager.instance().storeImage(
+ newUri,
+ CropImage.this,
+ getContentResolver(),
+ 0, // TODO fix this orientation
+ mCroppedImage,
+ null);
+
+ cancelable.get();
+ setResult(RESULT_OK,
+ (new Intent()).setAction(newUri.toString())
+ .putExtras(extras));
+ } catch (Exception ex) {
+ // basically ignore this or put up
+ // some ui saying we failed
+ }
+ }
+ }
+ finish();
+ }
+ };
+ Thread t = new Thread(r);
+ t.start();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ Handler mHandler = new Handler();
+
+ Runnable mRunFaceDetection = new Runnable() {
+ float mScale = 1F;
+ RectF mUnion = null;
+ Matrix mImageMatrix;
+ FaceDetector.Face[] mFaces = new FaceDetector.Face[3];
+ int mNumFaces;
+
+ private void handleFace(FaceDetector.Face f) {
+ PointF midPoint = new PointF();
+
+ int r = ((int)(f.eyesDistance() * mScale)) * 2 ;
+ f.getMidPoint(midPoint);
+ midPoint.x *= mScale;
+ midPoint.y *= mScale;
+
+ int midX = (int) midPoint.x;
+ int midY = (int) midPoint.y;
+
+ HighlightView hv = makeHighlightView();
+
+ int width = mBitmap.getWidth();
+ int height = mBitmap.getHeight();
+
+ Rect imageRect = new Rect(0, 0, width, height);
+
+ RectF faceRect = new RectF(midX, midY, midX, midY);
+ faceRect.inset(-r, -r);
+ if (faceRect.left < 0)
+ faceRect.inset(-faceRect.left, -faceRect.left);
+
+ if (faceRect.top < 0)
+ faceRect.inset(-faceRect.top, -faceRect.top);
+
+ if (faceRect.right > imageRect.right)
+ faceRect.inset(faceRect.right - imageRect.right, faceRect.right - imageRect.right);
+
+ if (faceRect.bottom > imageRect.bottom)
+ faceRect.inset(faceRect.bottom - imageRect.bottom, faceRect.bottom - imageRect.bottom);
+
+ hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop, mAspectX != 0 && mAspectY != 0);
+
+ if (mUnion == null) {
+ mUnion = new RectF(faceRect);
+ } else {
+ mUnion.union(faceRect);
+ }
+
+ mImageView.add(hv);
+ }
+
+ private HighlightView makeHighlightView() {
+ return new HighlightView(mImageView);
+ }
+
+ private void makeDefault() {
+ HighlightView hv = makeHighlightView();
+
+ int width = mBitmap.getWidth();
+ int height = mBitmap.getHeight();
+
+ Rect imageRect = new Rect(0, 0, width, height);
+
+ // make the default size about 4/5 of the width or height
+ int cropWidth = Math.min(width, height) * 4 / 5;
+ int cropHeight = cropWidth;
+
+ if (mAspectX != 0 && mAspectY != 0) {
+ if (mAspectX > mAspectY) {
+ cropHeight = cropWidth * mAspectY / mAspectX;
+// Log.v(TAG, "adjusted cropHeight to " + cropHeight);
+ } else {
+ cropWidth = cropHeight * mAspectX / mAspectY;
+// Log.v(TAG, "adjusted cropWidth to " + cropWidth);
+ }
+ }
+
+ int x = (width - cropWidth) / 2;
+ int y = (height - cropHeight) / 2;
+
+ RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
+ hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop, mAspectX != 0 && mAspectY != 0);
+ mImageView.add(hv);
+ }
+
+ private Bitmap prepareBitmap() {
+ if (mBitmap == null)
+ return null;
+
+ // scale the image down for faster face detection
+ // 256 pixels wide is enough.
+ if (mBitmap.getWidth() > 256) {
+ mScale = 256.0F / (float) mBitmap.getWidth();
+ }
+ Matrix matrix = new Matrix();
+ matrix.setScale(mScale, mScale);
+ Bitmap faceBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap
+ .getWidth(), mBitmap.getHeight(), matrix, true);
+ return faceBitmap;
+ }
+
+ public void run() {
+ mImageMatrix = mImageView.getImageMatrix();
+ Bitmap faceBitmap = prepareBitmap();
+
+ mScale = 1.0F / mScale;
+ if (faceBitmap != null && mDoFaceDetection) {
+ FaceDetector detector = new FaceDetector(faceBitmap.getWidth(),
+ faceBitmap.getHeight(), mFaces.length);
+ mNumFaces = detector.findFaces(faceBitmap, mFaces);
+ if (Config.LOGV)
+ Log.v(TAG, "numFaces is " + mNumFaces);
+ }
+ mHandler.post(new Runnable() {
+ public void run() {
+ mWaitingToPick = mNumFaces > 1;
+ if (mNumFaces > 0) {
+ for (int i = 0; i < mNumFaces; i++) {
+ handleFace(mFaces[i]);
+ }
+ } else {
+ makeDefault();
+ }
+ mImageView.invalidate();
+ if (mImageView.mHighlightViews.size() == 1) {
+ mCrop = mImageView.mHighlightViews.get(0);
+ mCrop.setFocus(true);
+ }
+
+ closeProgressDialog();
+
+ if (mNumFaces > 1) {
+ Toast t = Toast.makeText(CropImage.this, R.string.multiface_crop_help, Toast.LENGTH_SHORT);
+ t.show();
+ }
+ }
+ });
+
+ }
+ };
+
+ @Override
+ public void onStop() {
+ closeProgressDialog();
+ super.onStop();
+ if (mAllImages != null)
+ mAllImages.deactivate();
+ }
+
+ private synchronized void closeProgressDialog() {
+ if (mFaceDetectionDialog != null) {
+ mFaceDetectionDialog.dismiss();
+ mFaceDetectionDialog = null;
+ }
+ if (mSavingProgressDialog != null) {
+ mSavingProgressDialog.dismiss();
+ mSavingProgressDialog = null;
+ }
+ }
+}
diff --git a/src/com/android/camera/DrmWallpaper.java b/src/com/android/camera/DrmWallpaper.java
new file mode 100644
index 0000000..10f33dc
--- /dev/null
+++ b/src/com/android/camera/DrmWallpaper.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2007 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;
+
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+
+/**
+ * Wallpaper picker for DRM images. This just redirects to the standard pick action.
+ */
+public class DrmWallpaper extends Wallpaper {
+
+ protected void formatIntent(Intent intent) {
+ super.formatIntent(intent);
+ intent.putExtra("pick-drm", true);
+ }
+
+}
diff --git a/src/com/android/camera/ErrorScreen.java b/src/com/android/camera/ErrorScreen.java
new file mode 100644
index 0000000..1018eb7
--- /dev/null
+++ b/src/com/android/camera/ErrorScreen.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2006 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;
+
+import android.content.Intent;
+import android.app.Activity;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.Bundle;
+import android.widget.TextView;
+
+
+/**
+ *
+ */
+public class ErrorScreen extends Activity
+{
+ int mError;
+ boolean mLogoutOnExit;
+ boolean mReconnectOnExit;
+ Handler mHandler = new Handler();
+
+ Runnable mCloseScreenCallback = new Runnable() {
+ public void run() {
+ finish();
+ }
+ };
+
+ @Override public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ resolveIntent();
+
+ String errMsg = null;
+
+ // PENDING: resourcify error messages!
+
+ switch (mError) {
+ default:
+ errMsg = "You need to setup your Picassa Web account first.";
+ break;
+ }
+
+ TextView tv = new TextView(this);
+ tv.setText(errMsg);
+ setContentView(tv);
+ }
+
+ @Override public void onResume() {
+ super.onResume();
+
+ mHandler.postAtTime(mCloseScreenCallback,
+ SystemClock.uptimeMillis() + 5000);
+
+ }
+
+ @Override public void onStop() {
+ super.onStop();
+ mHandler.removeCallbacks(mCloseScreenCallback);
+// startNextActivity();
+ }
+
+ void resolveIntent() {
+ Intent intent = getIntent();
+ mError = intent.getIntExtra("error", mError);
+
+ mLogoutOnExit = intent.getBooleanExtra("logout", mLogoutOnExit);
+ mReconnectOnExit = intent.getBooleanExtra("reconnect", mReconnectOnExit);
+ }
+
+// void startNextActivity() {
+// GTalkApp app = GTalkApp.getInstance();
+//
+// if (mLogoutOnExit) {
+// app.logout();
+// }
+// else if (mReconnectOnExit) {
+// app.showLogin(false);
+// }
+// }
+}
diff --git a/src/com/android/camera/ExifInterface.java b/src/com/android/camera/ExifInterface.java
new file mode 100644
index 0000000..2db021a
--- /dev/null
+++ b/src/com/android/camera/ExifInterface.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2007 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;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+import android.util.Config;
+import android.util.Log;
+
+// Wrapper for native Exif library
+
+public class ExifInterface {
+
+ private String mFilename;
+
+ // Constants used for the Orientation Exif tag.
+ static final int ORIENTATION_UNDEFINED = 0;
+ static final int ORIENTATION_NORMAL = 1;
+ static final int ORIENTATION_FLIP_HORIZONTAL = 2; // left right reversed mirror
+ static final int ORIENTATION_ROTATE_180 = 3;
+ static final int ORIENTATION_FLIP_VERTICAL = 4; // upside down mirror
+ static final int ORIENTATION_TRANSPOSE = 5; // flipped about top-left <--> bottom-right axis
+ static final int ORIENTATION_ROTATE_90 = 6; // rotate 90 cw to right it
+ static final int ORIENTATION_TRANSVERSE = 7; // flipped about top-right <--> bottom-left axis
+ static final int ORIENTATION_ROTATE_270 = 8; // rotate 270 to right it
+
+ // The Exif tag names
+ static final String TAG_ORIENTATION = "Orientation";
+ static final String TAG_DATE_TIME_ORIGINAL = "DateTimeOriginal";
+ static final String TAG_MAKE = "Make";
+ static final String TAG_MODEL = "Model";
+ static final String TAG_FLASH = "Flash";
+ static final String TAG_IMAGE_WIDTH = "ImageWidth";
+ static final String TAG_IMAGE_LENGTH = "ImageLength";
+
+ static final String TAG_GPS_LATITUDE = "GPSLatitude";
+ static final String TAG_GPS_LONGITUDE = "GPSLongitude";
+
+ static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
+ static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
+
+ private boolean mSavedAttributes = false;
+ private boolean mHasThumbnail = false;
+ private HashMap<String, String> mCachedAttributes = null;
+
+ static {
+ System.loadLibrary("exif");
+ }
+
+ public ExifInterface(String fileName) {
+ mFilename = fileName;
+ }
+
+ /**
+ * Given a HashMap of Exif tags and associated values, an Exif section in the JPG file
+ * is created and loaded with the tag data. saveAttributes() is expensive because it involves
+ * copying all the JPG data from one file to another and deleting the old file and renaming the other.
+ * It's best to collect all the attributes to write and make a single call rather than multiple
+ * calls for each attribute. You must call "commitChanges()" at some point to commit the changes.
+ */
+ public void saveAttributes(HashMap<String, String> attributes) {
+ // format of string passed to native C code:
+ // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
+ // example: "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
+ StringBuilder sb = new StringBuilder();
+ int size = attributes.size();
+ if (attributes.containsKey("hasThumbnail")) {
+ --size;
+ }
+ sb.append(size + " ");
+ Iterator keyIterator = attributes.keySet().iterator();
+ while (keyIterator.hasNext()) {
+ String key = (String)keyIterator.next();
+ if (key.equals("hasThumbnail")) {
+ continue; // this is a fake attribute not saved as an exif tag
+ }
+ String val = (String)attributes.get(key);
+ sb.append(key + "=");
+ sb.append(val.length() + " ");
+ sb.append(val);
+ }
+ String s = sb.toString();
+ if (android.util.Config.LOGV)
+ android.util.Log.v("camera", "saving exif data: " + s);
+ saveAttributesNative(mFilename, s);
+ mSavedAttributes = true;
+ }
+
+ /**
+ * Returns a HashMap loaded with the Exif attributes of the file. The key is the standard
+ * tag name and the value is the tag's value: e.g. Model -> Nikon. Numeric values are
+ * returned as strings.
+ */
+ public HashMap<String, String> getAttributes() {
+ if (mCachedAttributes != null) {
+ return mCachedAttributes;
+ }
+ // format of string passed from native C code:
+ // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
+ // example: "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
+ mCachedAttributes = new HashMap<String, String>();
+
+ String attrStr = getAttributesNative(mFilename);
+
+ // get count
+ int ptr = attrStr.indexOf(' ');
+ int count = Integer.parseInt(attrStr.substring(0, ptr));
+ ++ptr; // skip past the space between item count and the rest of the attributes
+
+ for (int i = 0; i < count; i++) {
+ // extract the attribute name
+ int equalPos = attrStr.indexOf('=', ptr);
+ String attrName = attrStr.substring(ptr, equalPos);
+ ptr = equalPos + 1; // skip past =
+
+ // extract the attribute value length
+ int lenPos = attrStr.indexOf(' ', ptr);
+ int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos));
+ ptr = lenPos + 1; // skip pas the space
+
+ // extract the attribute value
+ String attrValue = attrStr.substring(ptr, ptr + attrLen);
+ ptr += attrLen;
+
+ if (attrName.equals("hasThumbnail")) {
+ mHasThumbnail = attrValue.equalsIgnoreCase("true");
+ } else {
+ mCachedAttributes.put(attrName, attrValue);
+ }
+ }
+ return mCachedAttributes;
+ }
+
+ /**
+ * Given a numerical orientation, return a human-readable string describing the orientation.
+ */
+ static public String orientationToString(int orientation) {
+ // TODO: this function needs to be localized and use string resource ids rather than strings
+ String orientationString;
+ switch (orientation) {
+ case ORIENTATION_NORMAL: orientationString = "Normal"; break;
+ case ORIENTATION_FLIP_HORIZONTAL: orientationString = "Flipped horizontal"; break;
+ case ORIENTATION_ROTATE_180: orientationString = "Rotated 180 degrees"; break;
+ case ORIENTATION_FLIP_VERTICAL: orientationString = "Upside down mirror"; break;
+ case ORIENTATION_TRANSPOSE: orientationString = "Transposed"; break;
+ case ORIENTATION_ROTATE_90: orientationString = "Rotated 90 degrees"; break;
+ case ORIENTATION_TRANSVERSE: orientationString = "Transversed"; break;
+ case ORIENTATION_ROTATE_270: orientationString = "Rotated 270 degrees"; break;
+ default: orientationString = "Undefined"; break;
+ }
+ return orientationString;
+ }
+
+ /**
+ * Copies the thumbnail data out of the filename and puts it in the Exif data associated
+ * with the file used to create this object. You must call "commitChanges()" at some point
+ * to commit the changes.
+ */
+ public boolean appendThumbnail(String thumbnailFileName) {
+ if (!mSavedAttributes) {
+ throw new RuntimeException("Must call saveAttributes before calling appendThumbnail");
+ }
+ mHasThumbnail = appendThumbnailNative(mFilename, thumbnailFileName);
+ return mHasThumbnail;
+ }
+
+ /**
+ * Saves the changes (added Exif tags, added thumbnail) to the JPG file. You have to call
+ * saveAttributes() before committing the changes.
+ */
+ public void commitChanges() {
+ if (!mSavedAttributes) {
+ throw new RuntimeException("Must call saveAttributes before calling commitChanges");
+ }
+ commitChangesNative(mFilename);
+ }
+
+ public boolean hasThumbnail() {
+ if (!mSavedAttributes) {
+ getAttributes();
+ }
+ return mHasThumbnail;
+ }
+
+ public byte[] getThumbnail() {
+ return getThumbnailNative(mFilename);
+ }
+
+ static public String convertRationalLatLonToDecimalString(String rationalString, String ref, boolean usePositiveNegative) {
+ try {
+ String [] parts = rationalString.split(",");
+
+ String [] pair;
+ pair = parts[0].split("/");
+ int degrees = (int) (Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim()));
+
+ pair = parts[1].split("/");
+ int minutes = (int) ((Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim())));
+
+ pair = parts[2].split("/");
+ float seconds = Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim());
+
+ float result = degrees + (minutes/60F) + (seconds/(60F*60F));
+
+ String preliminaryResult = String.valueOf(result);
+ if (usePositiveNegative) {
+ String neg = (ref.equals("S") || ref.equals("E")) ? "-" : "";
+ return neg + preliminaryResult;
+ } else {
+ return preliminaryResult + String.valueOf((char)186) + " " + ref;
+ }
+ } catch (Exception ex) {
+ // if for whatever reason we can't parse the lat long then return null
+ return null;
+ }
+ }
+
+ static public String makeLatLongString(double d) {
+ d = Math.abs(d);
+
+ int degrees = (int) d;
+
+ double remainder = d - (double)degrees;
+ int minutes = (int) (remainder * 60D);
+ int seconds = (int) (((remainder * 60D) - minutes) * 60D * 1000D); // really seconds * 1000
+
+ String retVal = degrees + "/1," + minutes + "/1," + (int)seconds + "/1000";
+ return retVal;
+ }
+
+ static public String makeLatStringRef(double lat) {
+ return lat >= 0D ? "N" : "S";
+ }
+
+ static public String makeLonStringRef(double lon) {
+ return lon >= 0D ? "W" : "E";
+ }
+
+ private native boolean appendThumbnailNative(String fileName, String thumbnailFileName);
+
+ private native void saveAttributesNative(String fileName, String compressedAttributes);
+
+ private native String getAttributesNative(String fileName);
+
+ private native void commitChangesNative(String fileName);
+
+ private native byte[] getThumbnailNative(String fileName);
+}
diff --git a/src/com/android/camera/GalleryPicker.java b/src/com/android/camera/GalleryPicker.java
new file mode 100644
index 0000000..9c687c8
--- /dev/null
+++ b/src/com/android/camera/GalleryPicker.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.StatFs;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore.Images;
+import android.util.Config;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GalleryPicker extends Activity {
+ static private final String TAG = "GalleryPicker";
+
+ private View mNoImagesView;
+ GridView mGridView;
+ Drawable mFrameGalleryMask;
+ Drawable mCellOutline;
+ Drawable mVideoOverlay;
+
+ BroadcastReceiver mReceiver;
+ GalleryPickerAdapter mAdapter;
+
+ Dialog mMediaScanningDialog;
+
+ SharedPreferences mPrefs;
+
+ boolean mPausing = false;
+
+ private static long LOW_STORAGE_THRESHOLD = 1024 * 1024 * 2;
+
+ public GalleryPicker() {
+ }
+
+ private void rebake(boolean unmounted, boolean scanning) {
+ if (mMediaScanningDialog != null) {
+ mMediaScanningDialog.cancel();
+ mMediaScanningDialog = null;
+ }
+ if (scanning) {
+ mMediaScanningDialog = ProgressDialog.show(
+ this,
+ null,
+ getResources().getString(R.string.wait),
+ true,
+ true);
+ }
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ mAdapter.init(!unmounted && !scanning);
+ }
+
+ if (!unmounted) {
+ // Warn the user if space is getting low
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+
+ // Check available space only if we are writable
+ if (ImageManager.hasStorage()) {
+ String storageDirectory = Environment.getExternalStorageDirectory().toString();
+ StatFs stat = new StatFs(storageDirectory);
+ long remaining = (long)stat.getAvailableBlocks() * (long)stat.getBlockSize();
+ if (remaining < LOW_STORAGE_THRESHOLD) {
+
+ mHandler.post(new Runnable() {
+ public void run() {
+ Toast.makeText(GalleryPicker.this.getApplicationContext(),
+ R.string.not_enough_space, 5000).show();
+ }
+ });
+ }
+ }
+ }
+ });
+ t.start();
+ }
+
+ // If we just have zero or one folder, open it. (We shouldn't have just one folder
+ // any more, but we can have zero folders.)
+ mNoImagesView.setVisibility(View.GONE);
+ if (!scanning) {
+ int numItems = mAdapter.mItems.size();
+ if (numItems == 0) {
+ mNoImagesView.setVisibility(View.VISIBLE);
+ } else if (numItems == 1) {
+ mAdapter.mItems.get(0).launch(this);
+ finish();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ setContentView(R.layout.gallerypicker);
+
+ mNoImagesView = findViewById(R.id.no_images);
+ mGridView = (GridView) findViewById(R.id.albums);
+ mGridView.setSelector(android.R.color.transparent);
+
+ mReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Config.LOGV) Log.v(TAG, "onReceiveIntent " + intent.getAction());
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ // SD card available
+ // TODO put up a "please wait" message
+ // TODO also listen for the media scanner finished message
+ } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+ // SD card unavailable
+ if (Config.LOGV) Log.v(TAG, "sd card no longer available");
+ Toast.makeText(GalleryPicker.this, getResources().getString(R.string.wait), 5000);
+ rebake(true, false);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+ Toast.makeText(GalleryPicker.this, getResources().getString(R.string.wait), 5000);
+ rebake(false, true);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+ if (Config.LOGV)
+ Log.v(TAG, "rebake because of ACTION_MEDIA_SCANNER_FINISHED");
+ rebake(false, false);
+ } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ if (Config.LOGV)
+ Log.v(TAG, "rebake because of ACTION_MEDIA_EJECT");
+ rebake(true, false);
+ }
+ }
+ };
+
+ mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ launchFolderGallery(position);
+ }
+ });
+ mGridView.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
+ public void onCreateContextMenu(ContextMenu menu, View v, final ContextMenu.ContextMenuInfo menuInfo) {
+ int position = ((AdapterContextMenuInfo)menuInfo).position;
+ menu.setHeaderTitle(mAdapter.baseTitleForPosition(position));
+ if ((mAdapter.getIncludeMediaTypes(position) & ImageManager.INCLUDE_IMAGES) != 0) {
+ menu.add(0, 207, 0, R.string.slide_show)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo;
+ int position = info.position;
+
+ Uri targetUri;
+ synchronized (mAdapter.mItems) {
+ if (position < 0 || position >= mAdapter.mItems.size()) {
+ return true;
+ }
+ // the mFirstImageUris list includes the "all" uri
+ targetUri = mAdapter.mItems.get(position).mFirstImageUri;
+ }
+ if (targetUri != null && position > 0) {
+ targetUri = targetUri.buildUpon().appendQueryParameter("bucketId",
+ mAdapter.mItems.get(info.position).mId).build();
+ }
+ // Log.v(TAG, "URI to launch slideshow " + targetUri);
+ Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
+ intent.putExtra("slideshow", true);
+ startActivity(intent);
+ return true;
+ }
+ });
+ }
+ menu.add(0, 208, 0, R.string.view)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo;
+ launchFolderGallery(info.position);
+ return true;
+ }
+ });
+ }
+ });
+
+ ImageManager.ensureOSXCompatibleFolder();
+ }
+
+ private void launchFolderGallery(int position) {
+ mAdapter.mItems.get(position).launch(this);
+ }
+
+ class ItemInfo {
+ Bitmap bitmap;
+ int count;
+ }
+
+ static class Item implements Comparable<Item>{
+ // The type is also used as the sort order
+ public final static int TYPE_NONE = -1;
+ public final static int TYPE_ALL_IMAGES = 0;
+ public final static int TYPE_ALL_VIDEOS = 1;
+ public final static int TYPE_CAMERA_IMAGES = 2;
+ public final static int TYPE_CAMERA_VIDEOS = 3;
+ public final static int TYPE_NORMAL_FOLDERS = 4;
+
+ public int mType;
+ public String mId;
+ public String mName;
+ public Uri mFirstImageUri;
+ public ItemInfo mThumb;
+
+ public Item(int type, String id, String name) {
+ mType = type;
+ mId = id;
+ mName = name;
+ }
+
+ public boolean needsBucketId() {
+ return mType >= TYPE_CAMERA_IMAGES;
+ }
+
+ public void launch(Activity activity) {
+ android.net.Uri uri = Images.Media.INTERNAL_CONTENT_URI;
+ if (needsBucketId()) {
+ uri = uri.buildUpon().appendQueryParameter("bucketId",mId).build();
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.putExtra("windowTitle", mName);
+ intent.putExtra("mediaTypes", getIncludeMediaTypes());
+ activity.startActivity(intent);
+ }
+
+ public int getIncludeMediaTypes() {
+ return convertItemTypeToIncludedMediaType(mType);
+ }
+
+ public static int convertItemTypeToIncludedMediaType(int itemType) {
+ switch (itemType) {
+ case TYPE_ALL_IMAGES:
+ case TYPE_CAMERA_IMAGES:
+ return ImageManager.INCLUDE_IMAGES;
+ case TYPE_ALL_VIDEOS:
+ case TYPE_CAMERA_VIDEOS:
+ return ImageManager.INCLUDE_VIDEOS;
+ case TYPE_NORMAL_FOLDERS:
+ default:
+ return ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS;
+ }
+ }
+
+ public int getOverlay() {
+ switch (mType) {
+ case TYPE_ALL_IMAGES:
+ case TYPE_CAMERA_IMAGES:
+ return R.drawable.frame_overlay_gallery_camera;
+ case TYPE_ALL_VIDEOS:
+ case TYPE_CAMERA_VIDEOS:
+ return R.drawable.frame_overlay_gallery_video;
+ case TYPE_NORMAL_FOLDERS:
+ return R.drawable.frame_overlay_gallery_folder;
+ default:
+ return -1;
+ }
+ }
+
+ // sort based on the sort order, then the case-insensitive display name, then the id.
+ public int compareTo(Item other) {
+ int x = mType - other.mType;
+ if (x == 0) {
+ x = mName.compareToIgnoreCase(other.mName);
+ if (x == 0) {
+ x = mId.compareTo(other.mId);
+ }
+ }
+ return x;
+ }
+ }
+
+ class GalleryPickerAdapter extends BaseAdapter {
+ ArrayList<Item> mItems = new ArrayList<Item>();
+
+ boolean mDone = false;
+ CameraThread mWorkerThread;
+
+ public void init(boolean assumeMounted) {
+ mItems.clear();
+
+ ImageManager.IImageList images;
+ if (assumeMounted) {
+ images = ImageManager.instance().allImages(
+ GalleryPicker.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_DESCENDING);
+ } else {
+ images = ImageManager.instance().emptyImageList();
+ }
+
+ if (mWorkerThread != null) {
+ try {
+ mDone = true;
+ if (Config.LOGV)
+ Log.v(TAG, "about to call join on thread " + mWorkerThread.getId());
+ mWorkerThread.join();
+ } finally {
+ mWorkerThread = null;
+ }
+ }
+
+ String cameraItem = ImageManager.CAMERA_IMAGE_BUCKET_ID;
+ final HashMap<String, String> hashMap = images.getBucketIds();
+ String cameraBucketId = null;
+ for (Map.Entry<String, String> entry: hashMap.entrySet()) {
+ String key = entry.getKey();
+ if (key == null) {
+ continue;
+ }
+ if (key.equals(cameraItem)) {
+ cameraBucketId = key;
+ } else {
+ mItems.add(new Item(Item.TYPE_NORMAL_FOLDERS, key, entry.getValue()));
+ }
+ }
+ images.deactivate();
+ notifyDataSetInvalidated();
+
+ // Conditionally add all-images and all-videos folders.
+ addBucket(Item.TYPE_ALL_IMAGES, null,
+ Item.TYPE_CAMERA_IMAGES, cameraBucketId, R.string.all_images);
+ addBucket(Item.TYPE_ALL_VIDEOS, null,
+ Item.TYPE_CAMERA_VIDEOS, cameraBucketId, R.string.all_videos);
+
+ if (cameraBucketId != null) {
+ addBucket(Item.TYPE_CAMERA_IMAGES, cameraBucketId,
+ R.string.gallery_camera_bucket_name);
+ addBucket(Item.TYPE_CAMERA_VIDEOS, cameraBucketId,
+ R.string.gallery_camera_videos_bucket_name);
+ }
+
+ java.util.Collections.sort(mItems);
+
+ mDone = false;
+ mWorkerThread = new CameraThread(new Runnable() {
+ public void run() {
+ try {
+ // no images, nothing to do
+ if (mItems.size() == 0)
+ return;
+
+ for (int i = 0; i < mItems.size() && !mDone; i++) {
+ final Item item = mItems.get(i);
+ ImageManager.IImageList list = createImageList(
+ item.getIncludeMediaTypes(), item.mId);
+ try {
+ if (mPausing) {
+ break;
+ }
+ if (list.getCount() > 0)
+ item.mFirstImageUri = list.getImageAt(0).fullSizeImageUri();
+
+ final Bitmap b = makeMiniThumbBitmap(142, 142, list);
+ final int pos = i;
+ final int count = list.getCount();
+ final Thread currentThread = Thread.currentThread();
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (mPausing || currentThread != mWorkerThread.realThread()) {
+ if (b != null) {
+ b.recycle();
+ }
+ return;
+ }
+
+ ItemInfo info = new ItemInfo();
+ info.bitmap = b;
+ info.count = count;
+ item.mThumb = info;
+
+ final GridView grid = GalleryPicker.this.mGridView;
+ final int firstVisible = grid.getFirstVisiblePosition();
+
+ // Minor optimization -- only notify if the specified position is visible
+ if ((pos >= firstVisible) && (pos < firstVisible + grid.getChildCount())) {
+ GalleryPickerAdapter.this.notifyDataSetChanged();
+ }
+ }
+ });
+ } finally {
+ list.deactivate();
+ }
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception generating collage views ", ex);
+ }
+ }
+ });
+ mWorkerThread.start();
+ mWorkerThread.toBackground();
+ }
+
+ /**
+ * Add a bucket, but only if it's interesting.
+ * Interesting means non-empty and not duplicated by the
+ * corresponding camera bucket.
+ */
+ private void addBucket(int itemType, String bucketId,
+ int cameraItemType, String cameraBucketId,
+ int labelId) {
+ int itemCount = bucketItemCount(
+ Item.convertItemTypeToIncludedMediaType(itemType), bucketId);
+ if (itemCount == 0) {
+ return; // Bucket is empty, so don't show it.
+ }
+ int cameraItemCount = 0;
+ if (cameraBucketId != null) {
+ cameraItemCount = bucketItemCount(
+ Item.convertItemTypeToIncludedMediaType(cameraItemType), cameraBucketId);
+ }
+ if (cameraItemCount == itemCount) {
+ return; // Bucket is the same as the camera bucket, so don't show it.
+ }
+ mItems.add(new Item(itemType, bucketId, getResources().getString(labelId)));
+ }
+
+ /**
+ * Add a bucket, but only if it's interesting.
+ * Interesting means non-empty.
+ */
+ private void addBucket(int itemType, String bucketId,
+ int labelId) {
+ if (!isEmptyBucket(Item.convertItemTypeToIncludedMediaType(itemType), bucketId)) {
+ mItems.add(new Item(itemType, bucketId, getResources().getString(labelId)));
+ }
+ }
+
+ public int getCount() {
+ return mItems.size();
+ }
+
+ public Object getItem(int position) {
+ return null;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ private String baseTitleForPosition(int position) {
+ return mItems.get(position).mName;
+ }
+
+ private int getIncludeMediaTypes(int position) {
+ return mItems.get(position).getIncludeMediaTypes();
+ }
+
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View v;
+
+ if (convertView == null) {
+ LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = vi.inflate(R.layout.gallery_picker_item, null);
+ } else {
+ v = convertView;
+ }
+
+ TextView titleView = (TextView) v.findViewById(R.id.title);
+
+ GalleryPickerItem iv = (GalleryPickerItem) v.findViewById(R.id.thumbnail);
+ iv.setOverlay(mItems.get(position).getOverlay());
+ ItemInfo info = mItems.get(position).mThumb;
+ if (info != null) {
+ iv.setImageBitmap(info.bitmap);
+ String title = baseTitleForPosition(position) + " (" + info.count + ")";
+ titleView.setText(title);
+ } else {
+ iv.setImageResource(android.R.color.transparent);
+ titleView.setText(baseTitleForPosition(position));
+ }
+
+ return v;
+ }
+ };
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPausing = true;
+ unregisterReceiver(mReceiver);
+
+ // free up some ram
+ mAdapter = null;
+ mGridView.setAdapter(null);
+ System.gc();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mPausing = false;
+
+ mAdapter = new GalleryPickerAdapter();
+ mGridView.setAdapter(mAdapter);
+ setBackgrounds(getResources());
+
+ boolean scanning = ImageManager.isMediaScannerScanning(this);
+ rebake(false, scanning);
+
+ // install an intent filter to receive SD card related events.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
+ intentFilter.addDataScheme("file");
+
+ registerReceiver(mReceiver, intentFilter);
+ MenuHelper.requestOrientation(this, mPrefs);
+ }
+
+
+
+ private void setBackgrounds(Resources r) {
+ mFrameGalleryMask = r.getDrawable(R.drawable.frame_gallery_preview_album_mask);
+
+ mCellOutline = r.getDrawable(android.R.drawable.gallery_thumb);
+ mVideoOverlay = r.getDrawable(R.drawable.ic_gallery_video_overlay);
+ }
+
+ Handler mHandler = new Handler();
+
+ private void placeImage(Bitmap image, Canvas c, Paint paint, int imageWidth, int widthPadding, int imageHeight, int heightPadding, int offsetX, int offsetY, int pos) {
+ int row = pos / 2;
+ int col = pos - (row * 2);
+
+ int xPos = (col * (imageWidth + widthPadding)) - offsetX;
+ int yPos = (row * (imageHeight + heightPadding)) - offsetY;
+
+ c.drawBitmap(image, xPos, yPos, paint);
+ }
+
+ private Bitmap makeMiniThumbBitmap(int width, int height, ImageManager.IImageList images) {
+ int count = images.getCount();
+ // We draw three different version of the folder image depending on the number of images in the folder.
+ // For a single image, that image draws over the whole folder.
+ // For two or three images, we draw the two most recent photos.
+ // For four or more images, we draw four photos.
+ final int padding = 4;
+ int imageWidth = width;
+ int imageHeight = height;
+ int offsetWidth = 0;
+ int offsetHeight = 0;
+
+ imageWidth = (imageWidth - padding) / 2; // 2 here because we show two images
+ imageHeight = (imageHeight - padding) / 2; // per row and column
+
+ final Paint p = new Paint();
+ final Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas c = new Canvas(b);
+
+ final Matrix m = new Matrix();
+
+ // draw the whole canvas as transparent
+ p.setColor(0x00000000);
+ c.drawPaint(p);
+
+ // draw the mask normally
+ p.setColor(0xFFFFFFFF);
+ mFrameGalleryMask.setBounds(0, 0, width, height);
+ mFrameGalleryMask.draw(c);
+
+ Paint pdpaint = new Paint();
+ pdpaint.setXfermode(new android.graphics.PorterDuffXfermode(
+ android.graphics.PorterDuff.Mode.SRC_IN));
+
+ pdpaint.setStyle(Paint.Style.FILL);
+ c.drawRect(0, 0, width, height, pdpaint);
+
+ for (int i = 0; i < 4; i++) {
+ if (mPausing) {
+ return null;
+ }
+
+ Bitmap temp = null;
+ ImageManager.IImage image = i < count ? images.getImageAt(i) : null;
+
+ if (image != null) {
+ temp = image.miniThumbBitmap();
+ }
+
+ if (temp != null) {
+ if (ImageManager.isVideo(image)) {
+ Bitmap newMap = temp.copy(temp.getConfig(), true);
+ Canvas overlayCanvas = new Canvas(newMap);
+ int overlayWidth = mVideoOverlay.getIntrinsicWidth();
+ int overlayHeight = mVideoOverlay.getIntrinsicHeight();
+ int left = (newMap.getWidth() - overlayWidth) / 2;
+ int top = (newMap.getHeight() - overlayHeight) / 2;
+ Rect newBounds = new Rect(left, top, left + overlayWidth, top + overlayHeight);
+ mVideoOverlay.setBounds(newBounds);
+ mVideoOverlay.draw(overlayCanvas);
+ temp.recycle();
+ temp = newMap;
+ }
+
+ Bitmap temp2 = ImageLoader.transform(m, temp, imageWidth, imageHeight, true);
+ if (temp2 != temp)
+ temp.recycle();
+ temp = temp2;
+ }
+
+ Bitmap thumb = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888);
+ Canvas tempCanvas = new Canvas(thumb);
+ if (temp != null)
+ tempCanvas.drawBitmap(temp, new Matrix(), new Paint());
+ mCellOutline.setBounds(0, 0, imageWidth, imageHeight);
+ mCellOutline.draw(tempCanvas);
+
+ placeImage(thumb, c, pdpaint, imageWidth, padding, imageHeight, padding, offsetWidth, offsetHeight, i);
+
+ thumb.recycle();
+
+ if (temp != null)
+ temp.recycle();
+ }
+
+ return b;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ MenuHelper.addCaptureMenuItems(menu, this);
+
+ menu.add(0, 0, 5, R.string.camerasettings)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent preferences = new Intent();
+ preferences.setClass(GalleryPicker.this, GallerySettings.class);
+ startActivity(preferences);
+ return true;
+ }
+ })
+ .setAlphabeticShortcut('p')
+ .setIcon(android.R.drawable.ic_menu_preferences);
+
+ return true;
+ }
+
+ private boolean isEmptyBucket(int mediaTypes, String bucketId) {
+ // TODO: Find a more efficient way of calculating this
+ ImageManager.IImageList list = createImageList(mediaTypes, bucketId);
+ try {
+ return list.isEmpty();
+ }
+ finally {
+ list.deactivate();
+ }
+ }
+
+ private int bucketItemCount(int mediaTypes, String bucketId) {
+ // TODO: Find a more efficient way of calculating this
+ ImageManager.IImageList list = createImageList(mediaTypes, bucketId);
+ try {
+ return list.getCount();
+ }
+ finally {
+ list.deactivate();
+ }
+ }
+ private ImageManager.IImageList createImageList(int mediaTypes, String bucketId) {
+ return ImageManager.instance().allImages(
+ this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ mediaTypes,
+ ImageManager.SORT_DESCENDING,
+ bucketId);
+ }
+}
diff --git a/src/com/android/camera/GalleryPickerItem.java b/src/com/android/camera/GalleryPickerItem.java
new file mode 100644
index 0000000..c3b5df1
--- /dev/null
+++ b/src/com/android/camera/GalleryPickerItem.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2008 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;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+public class GalleryPickerItem extends ImageView {
+
+ private Drawable mFrame;
+ private Rect mFrameBounds = new Rect();
+ private Drawable mOverlay;
+
+ public GalleryPickerItem(Context context) {
+ this(context, null);
+ }
+
+ public GalleryPickerItem(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GalleryPickerItem(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mFrame = getResources().getDrawable(R.drawable.frame_gallery_preview);
+ mFrame.setCallback(this);
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || (who == mFrame) || (who == mOverlay);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mFrame != null) {
+ int[] drawableState = getDrawableState();
+ mFrame.setState(drawableState);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ final Rect frameBounds = mFrameBounds;
+ if (frameBounds.isEmpty()) {
+ final int w = getWidth();
+ final int h = getHeight();
+
+ frameBounds.set(0, 0, w, h);
+ mFrame.setBounds(frameBounds);
+ if (mOverlay != null) {
+ mOverlay.setBounds(w - mOverlay.getIntrinsicWidth(),
+ h - mOverlay.getIntrinsicHeight(), w, h);
+ }
+ }
+
+ mFrame.draw(canvas);
+ if (mOverlay != null) {
+ mOverlay.draw(canvas);
+ }
+ }
+
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mFrameBounds.setEmpty();
+ }
+
+ public void setOverlay(int overlayId) {
+ if (overlayId >= 0) {
+ mOverlay = getResources().getDrawable(overlayId);
+ mFrameBounds.setEmpty();
+ } else {
+ mOverlay = null;
+ }
+ }
+}
diff --git a/src/com/android/camera/GallerySettings.java b/src/com/android/camera/GallerySettings.java
new file mode 100644
index 0000000..14cff3a
--- /dev/null
+++ b/src/com/android/camera/GallerySettings.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+/**
+ * GallerySettings
+ */
+public class GallerySettings extends PreferenceActivity
+{
+ public GallerySettings()
+ {
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.gallery_preferences);
+ }
+}
+
diff --git a/src/com/android/camera/HighlightView.java b/src/com/android/camera/HighlightView.java
new file mode 100644
index 0000000..408beab
--- /dev/null
+++ b/src/com/android/camera/HighlightView.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+import android.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+
+
+public class HighlightView
+{
+ private static final String TAG = "CropImage";
+ View mContext;
+ Path mPath;
+ Rect mViewDrawingRect = new Rect();
+
+ int mMotionMode;
+
+ public static final int GROW_NONE = (1 << 0);
+ public static final int GROW_LEFT_EDGE = (1 << 1);
+ public static final int GROW_RIGHT_EDGE = (1 << 2);
+ public static final int GROW_TOP_EDGE = (1 << 3);
+ public static final int GROW_BOTTOM_EDGE = (1 << 4);
+ public static final int MOVE = (1 << 5);
+
+ public HighlightView(View ctx)
+ {
+ super();
+ mContext = ctx;
+ mPath = new Path();
+ }
+
+ private void initHighlightView() {
+ android.content.res.Resources resources = mContext.getResources();
+ mResizeDrawableWidth = resources.getDrawable(R.drawable.camera_crop_width);
+ mResizeDrawableHeight = resources.getDrawable(R.drawable.camera_crop_height);
+ mResizeDrawableDiagonal = resources.getDrawable(R.drawable.indicator_autocrop);
+ }
+
+ boolean mIsFocused;
+ boolean mHidden;
+
+ public boolean hasFocus() {
+ return mIsFocused;
+ }
+
+ public void setFocus(boolean f) {
+ mIsFocused = f;
+ }
+
+ public void setHidden(boolean hidden) {
+ mHidden = hidden;
+ }
+
+ protected void draw(Canvas canvas) {
+ if (mHidden)
+ return;
+
+ canvas.save();
+ mPath.reset();
+ if (!hasFocus()) {
+ mOutlinePaint.setColor(0xFF000000);
+ canvas.drawRect(mDrawRect, mOutlinePaint);
+ } else {
+ mContext.getDrawingRect(mViewDrawingRect);
+ if (mCircle) {
+ float width = mDrawRect.width() - (getPaddingLeft() + getPaddingRight());
+ float height = mDrawRect.height() - (getPaddingTop() + getPaddingBottom());
+ mPath.addCircle(
+ mDrawRect.left + getPaddingLeft() + (width / 2),
+ mDrawRect.top + getPaddingTop() + (height / 2),
+ width / 2,
+ Path.Direction.CW);
+ mOutlinePaint.setColor(0xFFEF04D6);
+ } else {
+ mPath.addRect(new RectF(mDrawRect), Path.Direction.CW);
+ mOutlinePaint.setColor(0xFFFF8A00);
+ }
+ canvas.clipPath(mPath, Region.Op.DIFFERENCE);
+ canvas.drawRect(mViewDrawingRect, hasFocus() ? mFocusPaint : mNoFocusPaint);
+
+ canvas.restore();
+ canvas.drawPath(mPath, mOutlinePaint);
+
+ if (mMode == ModifyMode.Grow) {
+ if (mCircle) {
+ int width = mResizeDrawableDiagonal.getIntrinsicWidth();
+ int height = mResizeDrawableDiagonal.getIntrinsicHeight();
+
+ int d = (int) Math.round(Math.cos(/*45deg*/Math.PI/4D) * (mDrawRect.width() / 2D));
+ int x = mDrawRect.left + (mDrawRect.width() / 2) + d - width/2;
+ int y = mDrawRect.top + (mDrawRect.height() / 2) - d - height/2;
+ mResizeDrawableDiagonal.setBounds(x, y, x + mResizeDrawableDiagonal.getIntrinsicWidth(), y + mResizeDrawableDiagonal.getIntrinsicHeight());
+ mResizeDrawableDiagonal.draw(canvas);
+ } else {
+ int left = mDrawRect.left + 1;
+ int right = mDrawRect.right + 1;
+ int top = mDrawRect.top + 4;
+ int bottom = mDrawRect.bottom + 3;
+
+ int widthWidth = mResizeDrawableWidth.getIntrinsicWidth() / 2;
+ int widthHeight = mResizeDrawableWidth.getIntrinsicHeight()/ 2;
+ int heightHeight = mResizeDrawableHeight.getIntrinsicHeight()/ 2;
+ int heightWidth = mResizeDrawableHeight.getIntrinsicWidth()/ 2;
+
+ int xMiddle = mDrawRect.left + ((mDrawRect.right - mDrawRect.left) / 2);
+ int yMiddle = mDrawRect.top + ((mDrawRect.bottom - mDrawRect.top ) / 2);
+
+ mResizeDrawableWidth.setBounds(left-widthWidth, yMiddle-widthHeight, left+widthWidth, yMiddle+widthHeight);
+ mResizeDrawableWidth.draw(canvas);
+
+ mResizeDrawableWidth.setBounds(right-widthWidth, yMiddle-widthHeight, right+widthWidth, yMiddle+widthHeight);
+ mResizeDrawableWidth.draw(canvas);
+
+ mResizeDrawableHeight.setBounds(xMiddle-heightWidth, top-heightHeight, xMiddle+heightWidth, top+heightHeight);
+ mResizeDrawableHeight.draw(canvas);
+
+ mResizeDrawableHeight.setBounds(xMiddle-heightWidth, bottom-heightHeight, xMiddle+heightWidth, bottom+heightHeight);
+ mResizeDrawableHeight.draw(canvas);
+ }
+ }
+ }
+
+ }
+
+ float getPaddingTop() { return 0F; }
+ float getPaddingBottom() { return 0F; }
+ float getPaddingLeft() { return 0F; }
+ float getPaddingRight() { return 0F; }
+
+ public ModifyMode getMode() {
+ return mMode;
+ }
+
+ public void setMode(ModifyMode mode)
+ {
+ if (mode != mMode) {
+ mMode = mode;
+ mContext.invalidate();
+ }
+ }
+
+ public int getHit(float x, float y) {
+ Rect r = computeLayout();
+ final float hysteresis = 20F;
+ int retval = GROW_NONE;
+
+ if (mCircle) {
+ float distX = x - r.centerX();
+ float distY = y - r.centerY();
+ int distanceFromCenter = (int) Math.sqrt(distX*distX + distY*distY);
+ int radius = (int) (mDrawRect.width() - getPaddingLeft()) / 2;
+ int delta = distanceFromCenter - radius;
+ if (Math.abs(delta) <= hysteresis) {
+ if (Math.abs(distY) > Math.abs(distX)) {
+ if (distY < 0)
+ retval = GROW_TOP_EDGE;
+ else
+ retval = GROW_BOTTOM_EDGE;
+ } else {
+ if (distX < 0)
+ retval = GROW_LEFT_EDGE;
+ else
+ retval = GROW_RIGHT_EDGE;
+ }
+ } else if (distanceFromCenter < radius) {
+ retval = MOVE;
+ } else {
+ retval = GROW_NONE;
+ }
+// Log.v(TAG, "radius: " + radius + "; touchRadius: " + distanceFromCenter + "; distX: " + distX + "; distY: " + distY + "; retval: " + retval);
+ } else {
+ boolean verticalCheck = (y >= r.top - hysteresis) && (y < r.bottom + hysteresis);
+ boolean horizCheck = (x >= r.left - hysteresis) && (x < r.right + hysteresis);
+
+ if ((Math.abs(r.left - x) < hysteresis) && verticalCheck)
+ retval |= GROW_LEFT_EDGE;
+ if ((Math.abs(r.right - x) < hysteresis) && verticalCheck)
+ retval |= GROW_RIGHT_EDGE;
+ if ((Math.abs(r.top - y) < hysteresis) && horizCheck)
+ retval |= GROW_TOP_EDGE;
+ if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck)
+ retval |= GROW_BOTTOM_EDGE;
+
+ if (retval == GROW_NONE && r.contains((int)x, (int)y))
+ retval = MOVE;
+ }
+ return retval;
+ }
+
+ void handleMotion(int edge, float dx, float dy) {
+ Rect r = computeLayout();
+ if (edge == GROW_NONE) {
+ return;
+ } else if (edge == MOVE) {
+ moveBy(dx * (mCropRect.width() / r.width()),
+ dy * (mCropRect.height() / r.height()));
+ } else {
+ if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0)
+ dx = 0;
+
+ if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0)
+ dy = 0;
+
+ float xDelta = dx * (mCropRect.width() / r.width());
+ float yDelta = dy * (mCropRect.height() / r.height());
+ growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta,
+ (((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta);
+
+ }
+// Log.v(TAG, "ratio is now " + this.mCropRect.width() / this.mCropRect.height());
+ }
+
+ void moveBy(float dx, float dy) {
+ Rect invalRect = new Rect(mDrawRect);
+
+ mCropRect.offset(dx, dy);
+ mCropRect.offset(
+ Math.max(0, mImageRect.left - mCropRect.left),
+ Math.max(0, mImageRect.top - mCropRect.top));
+
+ mCropRect.offset(
+ Math.min(0, mImageRect.right - mCropRect.right),
+ Math.min(0, mImageRect.bottom - mCropRect.bottom));
+
+ mDrawRect = computeLayout();
+ invalRect.union(mDrawRect);
+ invalRect.inset(-10, -10);
+ mContext.invalidate(invalRect);
+ }
+
+ private void shift(RectF r, float dx, float dy) {
+ r.left += dx;
+ r.right += dx;
+ r.top += dy;
+ r.bottom += dy;
+ }
+
+ void growBy(float dx, float dy) {
+// Log.v(TAG, "growBy: " + dx + " " + dy + "; rect w/h is " + mCropRect.width() + " / " + mCropRect.height());
+ if (mMaintainAspectRatio) {
+ if (dx != 0) {
+ dy = dx / mInitialAspectRatio;
+ } else if (dy != 0) {
+ dx = dy * mInitialAspectRatio;
+ }
+ }
+
+ RectF r = new RectF(mCropRect);
+ if (dx > 0F && r.width() + 2 * dx > mImageRect.width()) {
+ float adjustment = (mImageRect.width() - r.width()) / 2F;
+ dx = adjustment;
+ if (mMaintainAspectRatio)
+ dy = dx / mInitialAspectRatio;
+ }
+ if (dy > 0F && r.height() + 2 * dy > mImageRect.height()) {
+ float adjustment = (mImageRect.height() - r.height()) / 2F;
+ dy = adjustment;
+ if (mMaintainAspectRatio)
+ dx = dy * mInitialAspectRatio;
+ }
+
+ r.inset(-dx, -dy);
+
+ float widthCap = 25F;
+ if (r.width() < 25) {
+ r.inset(-(25F-r.width())/2F, 0F);
+ }
+ float heightCap = mMaintainAspectRatio ? (widthCap / mInitialAspectRatio) : widthCap;
+ if (r.height() < heightCap) {
+ r.inset(0F, -(heightCap-r.height())/2F);
+ }
+
+ if (r.left < mImageRect.left) {
+ shift(r, mImageRect.left - r.left, 0F);
+ } else if (r.right > mImageRect.right) {
+ shift(r, -(r.right - mImageRect.right), 0);
+ }
+ if (r.top < mImageRect.top) {
+ shift(r, 0F, mImageRect.top - r.top);
+ } else if (r.bottom > mImageRect.bottom) {
+ shift(r, 0F, -(r.bottom - mImageRect.bottom));
+ }
+/*
+ RectF rCandidate = new RectF(r);
+ r.intersect(mImageRect);
+ if (mMaintainAspectRatio) {
+ if (r.left != rCandidate.left) {
+ Log.v(TAG, "bail 1");
+ return;
+ }
+ if (r.right != rCandidate.right) {
+ Log.v(TAG, "bail 2");
+ return;
+ }
+ if (r.top != rCandidate.top) {
+ Log.v(TAG, "bail 3");
+ return;
+ }
+ if (r.bottom != rCandidate.bottom) {
+ Log.v(TAG, "bail 4");
+ return;
+ }
+ }
+*/
+ mCropRect.set(r);
+ mDrawRect = computeLayout();
+ mContext.invalidate();
+ }
+
+ public Rect getCropRect() {
+ return new Rect((int)mCropRect.left, (int)mCropRect.top, (int)mCropRect.right, (int)mCropRect.bottom);
+ }
+
+ private Rect computeLayout() {
+ RectF r = new RectF(mCropRect.left, mCropRect.top, mCropRect.right, mCropRect.bottom);
+ mMatrix.mapRect(r);
+ return new Rect(Math.round(r.left), Math.round(r.top), Math.round(r.right), Math.round(r.bottom));
+ }
+
+ public void invalidate() {
+ mDrawRect = computeLayout();
+ }
+
+ public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean circle, boolean maintainAspectRatio) {
+ if (Config.LOGV) Log.v(TAG, "setup... " + imageRect + "; " + cropRect + "; maintain " + maintainAspectRatio + "; circle " + circle);
+ if (circle)
+ maintainAspectRatio = true;
+ mMatrix = new Matrix(m);
+
+ mCropRect = cropRect;
+ mImageRect = new RectF(imageRect);
+ mMaintainAspectRatio = maintainAspectRatio;
+ mCircle = circle;
+
+ mInitialAspectRatio = mCropRect.width() / mCropRect.height();
+ mDrawRect = computeLayout();
+
+ mFocusPaint.setARGB(125, 50, 50, 50);
+ mNoFocusPaint.setARGB(125, 50, 50, 50);
+ mOutlinePaint.setStrokeWidth(3F);
+ mOutlinePaint.setStyle(Paint.Style.STROKE);
+ mOutlinePaint.setAntiAlias(true);
+
+ mMode = ModifyMode.None;
+ initHighlightView();
+ }
+
+ public void modify(int keyCode, long repeatCount)
+ {
+ float factor = Math.max(.01F, Math.min(.1F, repeatCount * .01F));
+ float widthUnits = factor * (float)mContext.getWidth();
+ float heightUnits = widthUnits;
+
+ switch (keyCode)
+ {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (mMode == ModifyMode.Move)
+ moveBy(-widthUnits, 0);
+ else if (mMode == ModifyMode.Grow)
+ growBy(-widthUnits, 0);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (mMode == ModifyMode.Move)
+ moveBy(widthUnits, 0);
+ else if (mMode == ModifyMode.Grow)
+ growBy(widthUnits, 0);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (mMode == ModifyMode.Move)
+ moveBy(0, -heightUnits);
+ else if (mMode == ModifyMode.Grow)
+ growBy(0, -heightUnits);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (mMode == ModifyMode.Move)
+ moveBy(0, heightUnits);
+ else if (mMode == ModifyMode.Grow)
+ growBy(0, heightUnits);
+ break;
+ }
+ }
+
+ enum ModifyMode { None, Move,Grow };
+
+ ModifyMode mMode = ModifyMode.None;
+
+ Rect mDrawRect;
+ RectF mImageRect;
+ RectF mCropRect;
+ Matrix mMatrix;
+
+ boolean mMaintainAspectRatio = false;
+ float mInitialAspectRatio;
+ boolean mCircle = false;
+
+ Drawable mResizeDrawableWidth, mResizeDrawableHeight, mResizeDrawableDiagonal;
+
+ Paint mFocusPaint = new Paint();
+ Paint mNoFocusPaint = new Paint();
+ Paint mOutlinePaint = new Paint();
+}
diff --git a/src/com/android/camera/ImageGallery2.java b/src/com/android/camera/ImageGallery2.java
new file mode 100644
index 0000000..008eb21
--- /dev/null
+++ b/src/com/android/camera/ImageGallery2.java
@@ -0,0 +1,1848 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.content.BroadcastReceiver;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.Window;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.widget.Scroller;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+import com.android.camera.ImageManager.IImage;
+
+public class ImageGallery2 extends Activity {
+ private static final String TAG = "ImageGallery2";
+ private ImageManager.IImageList mAllImages;
+ private int mInclusion;
+ private boolean mSortAscending = false;
+ private View mNoImagesView;
+ public final static int CROP_MSG = 2;
+ public final static int VIEW_MSG = 3;
+ private static final String INSTANCE_STATE_TAG = "scrollY";
+
+ private Dialog mMediaScanningDialog;
+
+ private MenuItem mSlideShowItem;
+ private SharedPreferences mPrefs;
+ private long mVideoSizeLimit = Long.MAX_VALUE;
+
+ public ImageGallery2() {
+ }
+
+ BroadcastReceiver mReceiver = null;
+
+ Handler mHandler = new Handler();
+ boolean mLayoutComplete;
+ boolean mPausing = false;
+ boolean mStopThumbnailChecking = false;
+
+ CameraThread mThumbnailCheckThread;
+ GridViewSpecial mGvs;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ if (Config.LOGV) Log.v(TAG, "onCreate");
+ super.onCreate(icicle);
+
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); // must be called before setContentView()
+ setContentView(R.layout.image_gallery_2);
+
+ getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.custom_gallery_title);
+ if (Config.LOGV)
+ Log.v(TAG, "findView... " + findViewById(R.id.loading_indicator));
+
+ mGvs = (GridViewSpecial) findViewById(R.id.grid);
+ mGvs.requestFocus();
+
+ if (isPickIntent()) {
+ mVideoSizeLimit = getIntent().getLongExtra(
+ MediaStore.EXTRA_SIZE_LIMIT, Long.MAX_VALUE);
+ mGvs.mVideoSizeLimit = mVideoSizeLimit;
+ } else {
+ mVideoSizeLimit = Long.MAX_VALUE;
+ mGvs.mVideoSizeLimit = mVideoSizeLimit;
+ mGvs.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ if (mSelectedImageGetter.getCurrentImage() == null)
+ return;
+
+ boolean isImage = ImageManager.isImage(mSelectedImageGetter.getCurrentImage());
+ if (isImage) {
+ menu.add(0, 0, 0, R.string.view).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ mGvs.onSelect(mGvs.mCurrentSelection);
+ return true;
+ }
+ });
+ }
+
+ menu.setHeaderTitle(isImage ? R.string.context_menu_header
+ : R.string.video_context_menu_header);
+ if ((mInclusion & (ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS)) != 0) {
+ MenuHelper.MenuItemsResult r = MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL,
+ isImage,
+ ImageGallery2.this,
+ mHandler,
+ mDeletePhotoRunnable,
+ new MenuHelper.MenuInvoker() {
+ public void run(MenuHelper.MenuCallback cb) {
+ cb.run(mSelectedImageGetter.getCurrentImageUri(), mSelectedImageGetter.getCurrentImage());
+
+ mGvs.clearCache();
+ mGvs.invalidate();
+ mGvs.requestLayout();
+ mGvs.start();
+ mNoImagesView.setVisibility(mAllImages.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+ });
+ if (r != null)
+ r.gettingReadyToOpen(menu, mSelectedImageGetter.getCurrentImage());
+
+ if (isImage) {
+ addSlideShowMenu(menu, 1000);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ private MenuItem addSlideShowMenu(Menu menu, int position) {
+ return menu.add(0, 207, position, R.string.slide_show)
+ .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ ImageManager.IImage img = mSelectedImageGetter.getCurrentImage();
+ if (img == null) {
+ img = mAllImages.getImageAt(0);
+ if (img == null) {
+ return true;
+ }
+ }
+ Uri targetUri = img.fullSizeImageUri();
+ Uri thisUri = getIntent().getData();
+ if (thisUri != null) {
+ String bucket = thisUri.getQueryParameter("bucketId");
+ if (bucket != null) {
+ targetUri = targetUri.buildUpon().appendQueryParameter("bucketId", bucket).build();
+ }
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
+ intent.putExtra("slideshow", true);
+ startActivity(intent);
+ return true;
+ }
+ })
+ .setIcon(android.R.drawable.ic_menu_slideshow);
+ }
+
+ private Runnable mDeletePhotoRunnable = new Runnable() {
+ public void run() {
+ mGvs.clearCache();
+ IImage currentImage = mSelectedImageGetter.getCurrentImage();
+ if (currentImage != null) {
+ mAllImages.removeImage(currentImage);
+ }
+ mGvs.invalidate();
+ mGvs.requestLayout();
+ mGvs.start();
+ mNoImagesView.setVisibility(mAllImages.isEmpty() ? View.VISIBLE : View.GONE);
+ }
+ };
+
+ private SelectedImageGetter mSelectedImageGetter = new SelectedImageGetter() {
+ public Uri getCurrentImageUri() {
+ ImageManager.IImage image = getCurrentImage();
+ if (image != null)
+ return image.fullSizeImageUri();
+ else
+ return null;
+ }
+ public ImageManager.IImage getCurrentImage() {
+ int currentSelection = mGvs.mCurrentSelection;
+ if (currentSelection < 0 || currentSelection >= mAllImages.getCount())
+ return null;
+ else
+ return mAllImages.getImageAt(currentSelection);
+ }
+ };
+
+ @Override
+ public void onConfigurationChanged(android.content.res.Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mTargetScroll = mGvs.getScrollY();
+ }
+
+ private Runnable mLongPressCallback = new Runnable() {
+ public void run() {
+ mGvs.select(-2, false);
+ mGvs.showContextMenu();
+ }
+ };
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ mGvs.select(-2, false);
+ // The keyUp doesn't get called when the longpress menu comes up. We only get here when the user
+ // lets go of the center key before the longpress menu comes up.
+ mHandler.removeCallbacks(mLongPressCallback);
+
+ // open the photo
+ if (mSelectedImageGetter.getCurrentImage() != null) {
+ mGvs.onSelect(mGvs.mCurrentSelection);
+ }
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean handled = true;
+ int sel = mGvs.mCurrentSelection;
+ int columns = mGvs.mCurrentSpec.mColumns;
+ int count = mAllImages.getCount();
+ boolean pressed = false;
+ if (mGvs.mShowSelection) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (sel != count && (sel % columns < columns - 1)) {
+ sel += 1;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (sel > 0 && (sel % columns != 0)) {
+ sel -= 1;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if ((sel / columns) != 0) {
+ sel -= columns;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if ((sel / columns) != (sel+columns / columns)) {
+ sel = Math.min(count-1, sel + columns);
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ pressed = true;
+ mHandler.postDelayed(mLongPressCallback, ViewConfiguration.getLongPressTimeout());
+ break;
+ case KeyEvent.KEYCODE_DEL:
+ MenuHelper.deleteImage(this, mDeletePhotoRunnable,
+ mSelectedImageGetter.getCurrentImage());
+ break;
+ default:
+ handled = false;
+ break;
+ }
+ } else {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ int [] range = new int[2];
+ GridViewSpecial.ImageBlockManager ibm = mGvs.mImageBlockManager;
+ if (ibm != null) {
+ mGvs.mImageBlockManager.getVisibleRange(range);
+ int topPos = range[0];
+ android.graphics.Rect r = mGvs.getRectForPosition(topPos);
+ if (r.top < mGvs.getScrollY())
+ topPos += columns;
+ topPos = Math.min(count - 1, topPos);
+ sel = topPos;
+ }
+ break;
+ default:
+ handled = false;
+ break;
+ }
+ }
+ if (handled) {
+ mGvs.select(sel, pressed);
+ return true;
+ }
+ else
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private boolean isPickIntent() {
+ String action = getIntent().getAction();
+ return (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action));
+ }
+
+ private void launchCropperOrFinish(ImageManager.IImage img) {
+ Bundle myExtras = getIntent().getExtras();
+
+ if (MenuHelper.getImageFileSize(img) > mVideoSizeLimit) {
+
+ DialogInterface.OnClickListener buttonListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ };
+ new AlertDialog.Builder(this)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(R.string.file_info_title)
+ .setMessage(R.string.video_exceed_mms_limit)
+ .setNeutralButton(R.string.details_ok, buttonListener)
+ .show();
+ return;
+ }
+
+ String cropValue = myExtras != null ? myExtras.getString("crop") : null;
+ if (cropValue != null) {
+ Bundle newExtras = new Bundle();
+ if (cropValue.equals("circle"))
+ newExtras.putString("circleCrop", "true");
+
+ Intent cropIntent = new Intent();
+ cropIntent.setData(img.fullSizeImageUri());
+ cropIntent.setClass(this, CropImage.class);
+ cropIntent.putExtras(newExtras);
+
+ /* pass through any extras that were passed in */
+ cropIntent.putExtras(myExtras);
+ if (Config.LOGV) Log.v(TAG, "startSubActivity " + cropIntent);
+ startActivityForResult(cropIntent, CROP_MSG);
+ } else {
+ Intent result = new Intent(null, img.fullSizeImageUri());
+ if (myExtras != null && myExtras.getString("return-data") != null) {
+ Bitmap bitmap = img.fullSizeBitmap(1000);
+ if (bitmap != null)
+ result.putExtra("data", bitmap);
+ }
+ setResult(RESULT_OK, result);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (Config.LOGV)
+ Log.v(TAG, "onActivityResult: " + requestCode + "; resultCode is " + resultCode + "; data is " + data);
+ switch (requestCode) {
+ case MenuHelper.RESULT_COMMON_MENU_CROP: {
+ if (resultCode == RESULT_OK) {
+ // The CropImage activity passes back the Uri of the cropped image as
+ // the Action rather than the Data.
+ Uri dataUri = Uri.parse(data.getAction());
+ rebake(false,false);
+ IImage image = mAllImages.getImageForUri(dataUri);
+ if (image != null ) {
+ int rowId = image.getRow();
+ mGvs.select(rowId, false);
+ }
+ }
+ break;
+ }
+ case CROP_MSG: {
+ if (Config.LOGV) Log.v(TAG, "onActivityResult " + data);
+ if (resultCode == RESULT_OK) {
+ setResult(resultCode, data);
+ finish();
+ }
+ break;
+ }
+ case VIEW_MSG: {
+ if (Config.LOGV)
+ Log.v(TAG, "got VIEW_MSG with " + data);
+ ImageManager.IImage img = mAllImages.getImageForUri(data.getData());
+ launchCropperOrFinish(img);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPausing = true;
+ stopCheckingThumbnails();
+ mGvs.onPause();
+
+ if (mReceiver != null) {
+ unregisterReceiver(mReceiver);
+ mReceiver = null;
+ }
+ // Now that we've paused the threads that are using the cursor it is safe
+ // to deactivate it.
+ mAllImages.deactivate();
+ }
+
+ private void rebake(boolean unmounted, boolean scanning) {
+ stopCheckingThumbnails();
+ mGvs.clearCache();
+ if (mAllImages != null) {
+ mAllImages.deactivate();
+ mAllImages = null;
+ }
+ if (mMediaScanningDialog != null) {
+ mMediaScanningDialog.cancel();
+ mMediaScanningDialog = null;
+ }
+ if (scanning) {
+ mMediaScanningDialog = ProgressDialog.show(
+ this,
+ null,
+ getResources().getString(R.string.wait),
+ true,
+ true);
+ mAllImages = ImageManager.instance().emptyImageList();
+ } else {
+ mAllImages = allImages(!unmounted);
+ if (Config.LOGV)
+ Log.v(TAG, "mAllImages is now " + mAllImages);
+ mGvs.init(mHandler);
+ mGvs.start();
+ mGvs.requestLayout();
+ checkThumbnails();
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ super.onSaveInstanceState(state);
+ mTargetScroll = mGvs.getScrollY();
+ state.putInt(INSTANCE_STATE_TAG, mTargetScroll);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ super.onRestoreInstanceState(state);
+ mTargetScroll = state.getInt(INSTANCE_STATE_TAG, 0);
+ }
+
+ int mTargetScroll;
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ try {
+ mGvs.setSizeChoice(Integer.parseInt(mPrefs.getString("pref_gallery_size_key", "1")), mTargetScroll);
+
+ String sortOrder = mPrefs.getString("pref_gallery_sort_key", null);
+ if (sortOrder != null) {
+ mSortAscending = sortOrder.equals("ascending");
+ }
+ } catch (Exception ex) {
+
+ }
+ mPausing = false;
+
+ // install an intent filter to receive SD card related events.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
+ intentFilter.addDataScheme("file");
+
+ mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Config.LOGV) Log.v(TAG, "onReceiveIntent " + intent.getAction());
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ // SD card available
+ // TODO put up a "please wait" message
+ // TODO also listen for the media scanner finished message
+ } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+ // SD card unavailable
+ if (Config.LOGV) Log.v(TAG, "sd card no longer available");
+ Toast.makeText(ImageGallery2.this, getResources().getString(R.string.wait), 5000);
+ rebake(true, false);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+ Toast.makeText(ImageGallery2.this, getResources().getString(R.string.wait), 5000);
+ rebake(false, true);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+ if (Config.LOGV)
+ Log.v(TAG, "rebake because of ACTION_MEDIA_SCANNER_FINISHED");
+ rebake(false, false);
+ } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ if (Config.LOGV)
+ Log.v(TAG, "rebake because of ACTION_MEDIA_EJECT");
+ rebake(true, false);
+ }
+ }
+ };
+ registerReceiver(mReceiver, intentFilter);
+
+ MenuHelper.requestOrientation(this, mPrefs);
+
+ rebake(false, ImageManager.isMediaScannerScanning(this));
+ }
+
+ private void stopCheckingThumbnails() {
+ mStopThumbnailChecking = true;
+ if (mThumbnailCheckThread != null) {
+ mThumbnailCheckThread.join();
+ }
+ mStopThumbnailChecking = false;
+ }
+
+ private void checkThumbnails() {
+ final long startTime = System.currentTimeMillis();
+ final long t1 = System.currentTimeMillis();
+ mThumbnailCheckThread = new CameraThread(new Runnable() {
+ public void run() {
+ android.content.res.Resources resources = getResources();
+ final TextView progressTextView = (TextView) findViewById(R.id.loading_text);
+ final String progressTextFormatString = resources.getString(R.string.loading_progress_format_string);
+
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock mWakeLock =
+ pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
+ "ImageGallery2.checkThumbnails");
+ mWakeLock.acquire();
+ ImageManager.IImageList.ThumbCheckCallback r = new ImageManager.IImageList.ThumbCheckCallback() {
+ boolean mDidSetProgress = false;
+
+ public boolean checking(final int count, final int maxCount) {
+ if (mStopThumbnailChecking) {
+ return false;
+ }
+
+ if (!mLayoutComplete) {
+ return true;
+ }
+
+ if (!mDidSetProgress) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ findViewById(R.id.loading_indicator).setVisibility(View.VISIBLE);
+ }
+ });
+ mDidSetProgress = true;
+ }
+ mGvs.postInvalidate();
+
+ if (System.currentTimeMillis() - startTime > 1000) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ String s = String.format(progressTextFormatString, maxCount - count);
+ progressTextView.setText(s);
+ }
+ });
+ }
+
+ return !mPausing;
+ }
+ };
+ ImageManager.IImageList imageList = allImages(true);
+ imageList.checkThumbnails(r, imageList.getCount());
+ mWakeLock.release();
+ mThumbnailCheckThread = null;
+ mHandler.post(new Runnable() {
+ public void run() {
+ findViewById(R.id.loading_indicator).setVisibility(View.GONE);
+ }
+ });
+ long t2 = System.currentTimeMillis();
+ if (Config.LOGV)
+ Log.v(TAG, "check thumbnails thread finishing; took " + (t2-t1));
+ }
+ });
+
+ mThumbnailCheckThread.setName("check_thumbnails");
+ mThumbnailCheckThread.start();
+ mThumbnailCheckThread.toBackground();
+
+ ImageManager.IImageList list = allImages(true);
+ mNoImagesView.setVisibility(list.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(android.view.Menu menu) {
+ MenuItem item;
+ if (! isPickIntent()) {
+ MenuHelper.addCaptureMenuItems(menu, this);
+ if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
+ mSlideShowItem = addSlideShowMenu(menu, 5);
+
+ }
+ }
+
+ item = menu.add(0, 0, 1000, R.string.camerasettings);
+ item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent preferences = new Intent();
+ preferences.setClass(ImageGallery2.this, GallerySettings.class);
+ startActivity(preferences);
+ return true;
+ }
+ });
+ item.setAlphabeticShortcut('p');
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(android.view.Menu menu) {
+ if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
+ boolean videoSelected = isVideoSelected();
+ // TODO: Only enable slide show if there is at least one image in the folder.
+ if (mSlideShowItem != null) {
+ mSlideShowItem.setEnabled(!videoSelected);
+ }
+ }
+
+ return true;
+ }
+
+ private boolean isImageSelected() {
+ IImage image = mSelectedImageGetter.getCurrentImage();
+ return (image != null) && ImageManager.isImage(image);
+ }
+
+ private boolean isVideoSelected() {
+ IImage image = mSelectedImageGetter.getCurrentImage();
+ return (image != null) && ImageManager.isVideo(image);
+ }
+
+ private synchronized ImageManager.IImageList allImages(boolean assumeMounted) {
+ if (mAllImages == null) {
+ mNoImagesView = findViewById(R.id.no_images);
+
+ mInclusion = ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS;
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ String type = intent.resolveType(this);
+ if (Config.LOGV)
+ Log.v(TAG, "allImages... type is " + type);
+ TextView leftText = (TextView) findViewById(R.id.left_text);
+ if (type != null) {
+ if (type.equals("vnd.android.cursor.dir/image") || type.equals("image/*")) {
+ mInclusion = ImageManager.INCLUDE_IMAGES;
+ if (isPickIntent())
+ leftText.setText(R.string.pick_photos_gallery_title);
+ else
+ leftText.setText(R.string.photos_gallery_title);
+ }
+ if (type.equals("vnd.android.cursor.dir/video") || type.equals("video/*")) {
+ mInclusion = ImageManager.INCLUDE_VIDEOS;
+ if (isPickIntent())
+ leftText.setText(R.string.pick_videos_gallery_title);
+ else
+ leftText.setText(R.string.videos_gallery_title);
+ }
+ }
+ Bundle extras = intent.getExtras();
+ String title = extras!= null ? extras.getString("windowTitle") : null;
+ if (title != null && title.length() > 0) {
+ leftText.setText(title);
+ }
+
+ if (extras != null) {
+ mInclusion = (ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS)
+ & extras.getInt("mediaTypes", mInclusion);
+ }
+
+ if (extras != null && extras.getBoolean("pick-drm")) {
+ Log.d(TAG, "pick-drm is true");
+ mInclusion = ImageManager.INCLUDE_DRM_IMAGES;
+ }
+ }
+ if (Config.LOGV)
+ Log.v(TAG, "computing images... mSortAscending is " + mSortAscending
+ + "; assumeMounted is " + assumeMounted);
+ Uri uri = getIntent().getData();
+ if (!assumeMounted) {
+ mAllImages = ImageManager.instance().emptyImageList();
+ } else {
+ mAllImages = ImageManager.instance().allImages(
+ ImageGallery2.this,
+ getContentResolver(),
+ ImageManager.DataLocation.NONE,
+ mInclusion,
+ mSortAscending ? ImageManager.SORT_ASCENDING : ImageManager.SORT_DESCENDING,
+ uri != null ? uri.getQueryParameter("bucketId") : null);
+ }
+ }
+ return mAllImages;
+ }
+
+ public static class GridViewSpecial extends View {
+ private ImageGallery2 mGallery;
+ private Paint mGridViewPaint = new Paint();
+
+ private ImageBlockManager mImageBlockManager;
+ private Handler mHandler;
+
+ private LayoutSpec mCurrentSpec;
+ private boolean mShowSelection = false;
+ private int mCurrentSelection = -1;
+ private boolean mCurrentSelectionPressed;
+
+ private boolean mDirectionBiasDown = true;
+ private final static boolean sDump = false;
+
+ private long mVideoSizeLimit;
+
+ class LayoutSpec {
+ LayoutSpec(int cols, int w, int h, int leftEdgePadding, int rightEdgePadding, int intercellSpacing) {
+ mColumns = cols;
+ mCellWidth = w;
+ mCellHeight = h;
+ mLeftEdgePadding = leftEdgePadding;
+ mRightEdgePadding = rightEdgePadding;
+ mCellSpacing = intercellSpacing;
+ }
+ int mColumns;
+ int mCellWidth, mCellHeight;
+ int mLeftEdgePadding, mRightEdgePadding;
+ int mCellSpacing;
+ };
+
+ private LayoutSpec [] mCellSizeChoices = new LayoutSpec[] {
+ new LayoutSpec(0, 67, 67, 14, 14, 8),
+ new LayoutSpec(0, 92, 92, 14, 14, 8),
+ };
+ private int mSizeChoice = 1;
+
+ // Use a number like 100 or 200 here to allow the user to
+ // overshoot the start (top) or end (bottom) of the gallery.
+ // After overshooting the gallery will animate back to the
+ // appropriate location.
+ private int mMaxOvershoot = 0; // 100;
+ private int mMaxScrollY;
+ private int mMinScrollY;
+
+ private boolean mFling = true;
+ private Scroller mScroller = null;
+
+ private GestureDetector mGestureDetector;
+
+ public void dump() {
+ if (Config.LOGV){
+ Log.v(TAG, "mSizeChoice is " + mCellSizeChoices[mSizeChoice]);
+ Log.v(TAG, "mCurrentSpec.width / mCellHeight are " + mCurrentSpec.mCellWidth + " / " + mCurrentSpec.mCellHeight);
+ }
+ mImageBlockManager.dump();
+ }
+
+ private void init(Context context) {
+ mGridViewPaint.setColor(0xFF000000);
+ mGallery = (ImageGallery2) context;
+
+ setVerticalScrollBarEnabled(true);
+ initializeScrollbars(context.obtainStyledAttributes(android.R.styleable.View));
+
+ mGestureDetector = new GestureDetector(context, new SimpleOnGestureListener() {
+ @Override
+ public boolean onDown(MotionEvent e) {
+ if (mScroller != null && !mScroller.isFinished()) {
+ mScroller.forceFinished(true);
+ return false;
+ }
+
+ int pos = computeSelectedIndex(e);
+ if (pos >= 0 && pos < mGallery.mAllImages.getCount()) {
+ select(pos, true);
+ } else {
+ select(-1, false);
+ }
+ if (mImageBlockManager != null)
+ mImageBlockManager.repaintSelection(mCurrentSelection);
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ final float maxVelocity = 2500;
+ if (velocityY > maxVelocity)
+ velocityY = maxVelocity;
+ else if (velocityY < -maxVelocity)
+ velocityY = -maxVelocity;
+
+ select(-1, false);
+ if (mFling) {
+ mScroller = new Scroller(getContext());
+ mScroller.fling(0, mScrollY, 0, -(int)velocityY, 0, 0, 0, mMaxScrollY);
+ computeScroll();
+ }
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ performLongClick();
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ select(-1, false);
+ scrollBy(0, (int)distanceY);
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ super.onShowPress(e);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ select(mCurrentSelection, false);
+ int index = computeSelectedIndex(e);
+ if (index >= 0 && index < mGallery.mAllImages.getCount()) {
+ onSelect(index);
+ return true;
+ }
+ return false;
+ }
+ });
+// mGestureDetector.setIsLongpressEnabled(false);
+ }
+
+ public GridViewSpecial(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ public GridViewSpecial(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public GridViewSpecial(Context context) {
+ super(context);
+ init(context);
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ return mMaxScrollY + getHeight();
+ }
+
+ public void setSizeChoice(int choice, int scrollY) {
+ mSizeChoice = choice;
+ clearCache();
+ scrollTo(0, scrollY);
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ *
+ * @param newSel -2 means use old selection, -1 means remove selection
+ * @param newPressed
+ */
+ public void select(int newSel, boolean newPressed) {
+ if (newSel == -2) {
+ newSel = mCurrentSelection;
+ }
+ int oldSel = mCurrentSelection;
+ if ((oldSel == newSel) && (mCurrentSelectionPressed == newPressed))
+ return;
+
+ mShowSelection = (newSel != -1);
+ mCurrentSelection = newSel;
+ mCurrentSelectionPressed = newPressed;
+ if (mImageBlockManager != null) {
+ mImageBlockManager.repaintSelection(oldSel);
+ mImageBlockManager.repaintSelection(newSel);
+ }
+
+ if (newSel != -1)
+ ensureVisible(newSel);
+ }
+
+ private void ensureVisible(int pos) {
+ android.graphics.Rect r = getRectForPosition(pos);
+ int top = getScrollY();
+ int bot = top + getHeight();
+
+ if (r.bottom > bot) {
+ mScroller = new Scroller(getContext());
+ mScroller.startScroll(mScrollX, mScrollY, 0, r.bottom - getHeight() - mScrollY, 200);
+ computeScroll();
+ } else if (r.top < top) {
+ mScroller = new Scroller(getContext());
+ mScroller.startScroll(mScrollX, mScrollY, 0, r.top - mScrollY, 200);
+ computeScroll();
+ }
+ invalidate();
+ }
+
+ public void start() {
+ if (mGallery.mLayoutComplete) {
+ if (mImageBlockManager == null) {
+ mImageBlockManager = new ImageBlockManager();
+ mImageBlockManager.moveDataWindow(true, true);
+ }
+ }
+ }
+
+ public void onPause() {
+ mScroller = null;
+ if (mImageBlockManager != null) {
+ mImageBlockManager.onPause();
+ mImageBlockManager = null;
+ }
+ }
+
+ public void clearCache() {
+ if (mImageBlockManager != null) {
+ mImageBlockManager.onPause();
+ mImageBlockManager = null;
+ }
+ }
+
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mGallery.isFinishing() || mGallery.mPausing) {
+ return;
+ }
+
+ clearCache();
+
+ mCurrentSpec = mCellSizeChoices[mSizeChoice];
+ int oldColumnCount = mCurrentSpec.mColumns;
+
+ int width = right - left;
+ mCurrentSpec.mColumns = 1;
+ width -= mCurrentSpec.mCellWidth;
+ mCurrentSpec.mColumns += width / (mCurrentSpec.mCellWidth + mCurrentSpec.mCellSpacing);
+
+ mCurrentSpec.mLeftEdgePadding = ((right - left) - ((mCurrentSpec.mColumns - 1) * mCurrentSpec.mCellSpacing) - (mCurrentSpec.mColumns * mCurrentSpec.mCellWidth)) / 2;
+ mCurrentSpec.mRightEdgePadding = mCurrentSpec.mLeftEdgePadding;
+
+ int rows = (mGallery.mAllImages.getCount() + mCurrentSpec.mColumns - 1) / mCurrentSpec.mColumns;
+ mMaxScrollY = mCurrentSpec.mCellSpacing + (rows * (mCurrentSpec.mCellSpacing + mCurrentSpec.mCellHeight)) - (bottom - top) + mMaxOvershoot;
+ mMinScrollY = 0 - mMaxOvershoot;
+
+ mGallery.mLayoutComplete = true;
+
+ start();
+
+ if (mGallery.mSortAscending && mGallery.mTargetScroll == 0) {
+ scrollTo(0, mMaxScrollY - mMaxOvershoot);
+ } else {
+ if (oldColumnCount != 0) {
+ int y = mGallery.mTargetScroll * oldColumnCount / mCurrentSpec.mColumns;
+ Log.v(TAG, "target was " + mGallery.mTargetScroll + " now " + y);
+ scrollTo(0, y);
+ }
+ }
+ }
+
+ Bitmap scaleTo(int width, int height, Bitmap b) {
+ Matrix m = new Matrix();
+ m.setScale((float)width/64F, (float)height/64F);
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
+ if (b2 != b)
+ b.recycle();
+ return b2;
+ }
+
+ private class ImageBlockManager {
+ private ImageLoader mLoader;
+ private int mBlockCacheFirstBlockNumber = 0;
+
+ // mBlockCache is an array with a starting point which is not necessaryily
+ // zero. The first element of the array is indicated by mBlockCacheStartOffset.
+ private int mBlockCacheStartOffset = 0;
+ private ImageBlock [] mBlockCache;
+
+ private static final int sRowsPerPage = 6; // should compute this
+
+ private static final int sPagesPreCache = 2;
+ private static final int sPagesPostCache = 2;
+
+ private int mWorkCounter = 0;
+ private boolean mDone = false;
+
+ private Thread mWorkerThread;
+ private Bitmap mMissingImageThumbnailBitmap;
+ private Bitmap mMissingVideoThumbnailBitmap;
+
+ private Drawable mVideoOverlay;
+ private Drawable mVideoMmsErrorOverlay;
+
+ public void dump() {
+ synchronized (ImageBlockManager.this) {
+ StringBuilder line1 = new StringBuilder();
+ StringBuilder line2 = new StringBuilder();
+ if (Config.LOGV)
+ Log.v(TAG, ">>> mBlockCacheFirstBlockNumber: " + mBlockCacheFirstBlockNumber + " " + mBlockCacheStartOffset);
+ for (int i = 0; i < mBlockCache.length; i++) {
+ int index = (mBlockCacheStartOffset + i) % mBlockCache.length;
+ ImageBlock block = mBlockCache[index];
+ block.dump(line1, line2);
+ }
+ if (Config.LOGV){
+ Log.v(TAG, line1.toString());
+ Log.v(TAG, line2.toString());
+ }
+ }
+ }
+
+ ImageBlockManager() {
+ mLoader = new ImageLoader(mHandler, 1);
+
+ mBlockCache = new ImageBlock[sRowsPerPage * (sPagesPreCache + sPagesPostCache + 1)];
+ for (int i = 0; i < mBlockCache.length; i++) {
+ mBlockCache[i] = new ImageBlock();
+ }
+
+ mWorkerThread = new Thread(new Runnable() {
+ public void run() {
+ while (true) {
+ int workCounter;
+ synchronized (ImageBlockManager.this) {
+ workCounter = mWorkCounter;
+ }
+ if (mDone) {
+ if (Config.LOGV)
+ Log.v(TAG, "stopping the loader here " + Thread.currentThread().getName());
+ if (mLoader != null) {
+ mLoader.stop();
+ }
+ if (mBlockCache != null) {
+ for (int i = 0; i < mBlockCache.length; i++) {
+ ImageBlock block = mBlockCache[i];
+ if (block != null) {
+ block.recycleBitmaps();
+ mBlockCache[i] = null;
+ }
+ }
+ }
+ mBlockCache = null;
+ mBlockCacheStartOffset = 0;
+ mBlockCacheFirstBlockNumber = 0;
+
+ break;
+ }
+
+ loadNext();
+
+ synchronized (ImageBlockManager.this) {
+ if ((workCounter == mWorkCounter) && (! mDone)) {
+ try {
+ ImageBlockManager.this.wait();
+ } catch (InterruptedException ex) {
+ }
+ }
+ }
+ }
+ }
+ });
+ mWorkerThread.setName("image-block-manager");
+ mWorkerThread.start();
+ }
+
+ // Create this bitmap lazily, and only once for all the ImageBlocks to use
+ public Bitmap getErrorBitmap(ImageManager.IImage image) {
+ if (ImageManager.isImage(image)) {
+ if (mMissingImageThumbnailBitmap == null) {
+ mMissingImageThumbnailBitmap = BitmapFactory.decodeResource(GridViewSpecial.this.getResources(),
+ R.drawable.ic_missing_thumbnail_picture);
+ }
+ return mMissingImageThumbnailBitmap;
+ } else {
+ if (mMissingVideoThumbnailBitmap == null) {
+ mMissingVideoThumbnailBitmap = BitmapFactory.decodeResource(GridViewSpecial.this.getResources(),
+ R.drawable.ic_missing_thumbnail_video);
+ }
+ return mMissingVideoThumbnailBitmap;
+ }
+ }
+
+ private ImageBlock getBlockForPos(int pos) {
+ synchronized (ImageBlockManager.this) {
+ int blockNumber = pos / mCurrentSpec.mColumns;
+ int delta = blockNumber - mBlockCacheFirstBlockNumber;
+ if (delta >= 0 && delta < mBlockCache.length) {
+ int index = (mBlockCacheStartOffset + delta) % mBlockCache.length;
+ ImageBlock b = mBlockCache[index];
+ return b;
+ }
+ }
+ return null;
+ }
+
+ private void repaintSelection(int pos) {
+ synchronized (ImageBlockManager.this) {
+ ImageBlock b = getBlockForPos(pos);
+ if (b != null) {
+ b.repaintSelection();
+ }
+ }
+ }
+
+ private void onPause() {
+ synchronized (ImageBlockManager.this) {
+ mDone = true;
+ ImageBlockManager.this.notify();
+ }
+ if (mWorkerThread != null) {
+ try {
+ mWorkerThread.join();
+ mWorkerThread = null;
+ } catch (InterruptedException ex) {
+ //
+ }
+ }
+ Log.v(TAG, "/ImageBlockManager.onPause");
+ }
+
+ private void getVisibleRange(int [] range) {
+ // try to work around a possible bug in the VM wherein this appears to be null
+ try {
+ synchronized (ImageBlockManager.this) {
+ int blockLength = mBlockCache.length;
+ boolean lookingForStart = true;
+ ImageBlock prevBlock = null;
+ for (int i = 0; i < blockLength; i++) {
+ int index = (mBlockCacheStartOffset + i) % blockLength;
+ ImageBlock block = mBlockCache[index];
+ if (lookingForStart) {
+ if (block.mIsVisible) {
+ range[0] = block.mBlockNumber * mCurrentSpec.mColumns;
+ lookingForStart = false;
+ }
+ } else {
+ if (!block.mIsVisible || i == blockLength - 1) {
+ range[1] = (prevBlock.mBlockNumber * mCurrentSpec.mColumns) + mCurrentSpec.mColumns - 1;
+ break;
+ }
+ }
+ prevBlock = block;
+ }
+ }
+ } catch (NullPointerException ex) {
+ Log.e(TAG, "this is somewhat null, what up?");
+ range[0] = range[1] = 0;
+ }
+ }
+
+ private void loadNext() {
+ final int blockHeight = (mCurrentSpec.mCellSpacing + mCurrentSpec.mCellHeight);
+
+ final int firstVisBlock = Math.max(0, (mScrollY - mCurrentSpec.mCellSpacing) / blockHeight);
+ final int lastVisBlock = (mScrollY - mCurrentSpec.mCellSpacing + getHeight()) / blockHeight;
+
+// Log.v(TAG, "firstVisBlock == " + firstVisBlock + "; lastVisBlock == " + lastVisBlock);
+
+ synchronized (ImageBlockManager.this) {
+ ImageBlock [] blocks = mBlockCache;
+ int numBlocks = blocks.length;
+ if (mDirectionBiasDown) {
+ int first = (mBlockCacheStartOffset + (firstVisBlock - mBlockCacheFirstBlockNumber)) % blocks.length;
+ for (int i = 0; i < numBlocks; i++) {
+ int j = first + i;
+ if (j >= numBlocks)
+ j -= numBlocks;
+ ImageBlock b = blocks[j];
+ if (b.startLoading() > 0)
+ break;
+ }
+ } else {
+ int first = (mBlockCacheStartOffset + (lastVisBlock - mBlockCacheFirstBlockNumber)) % blocks.length;
+ for (int i = 0; i < numBlocks; i++) {
+ int j = first - i;
+ if (j < 0)
+ j += numBlocks;
+ ImageBlock b = blocks[j];
+ if (b.startLoading() > 0)
+ break;
+ }
+ }
+ if (sDump)
+ this.dump();
+ }
+ }
+
+ private void moveDataWindow(boolean directionBiasDown, boolean forceRefresh) {
+ final int blockHeight = (mCurrentSpec.mCellSpacing + mCurrentSpec.mCellHeight);
+
+ final int firstVisBlock = (mScrollY - mCurrentSpec.mCellSpacing) / blockHeight;
+ final int lastVisBlock = (mScrollY - mCurrentSpec.mCellSpacing + getHeight()) / blockHeight;
+
+ final int preCache = sPagesPreCache;
+ final int startBlock = Math.max(0, firstVisBlock - (preCache * sRowsPerPage));
+
+// Log.v(TAG, "moveDataWindow directionBiasDown == " + directionBiasDown + "; preCache is " + preCache);
+ synchronized (ImageBlockManager.this) {
+ boolean any = false;
+ ImageBlock [] blocks = mBlockCache;
+ int numBlocks = blocks.length;
+
+ int delta = startBlock - mBlockCacheFirstBlockNumber;
+
+ mBlockCacheFirstBlockNumber = startBlock;
+ if (Math.abs(delta) > numBlocks || forceRefresh) {
+ for (int i = 0; i < numBlocks; i++) {
+ int blockNum = startBlock + i;
+ blocks[i].setStart(blockNum);
+ any = true;
+ }
+ mBlockCacheStartOffset = 0;
+ } else if (delta > 0) {
+ mBlockCacheStartOffset += delta;
+ if (mBlockCacheStartOffset >= numBlocks)
+ mBlockCacheStartOffset -= numBlocks;
+
+ for (int i = delta; i > 0; i--) {
+ int index = (mBlockCacheStartOffset + numBlocks - i) % numBlocks;
+ int blockNum = mBlockCacheFirstBlockNumber + numBlocks - i;
+ blocks[index].setStart(blockNum);
+ any = true;
+ }
+ } else if (delta < 0) {
+ mBlockCacheStartOffset += delta;
+ if (mBlockCacheStartOffset < 0)
+ mBlockCacheStartOffset += numBlocks;
+
+ for (int i = 0; i < -delta; i++) {
+ int index = (mBlockCacheStartOffset + i) % numBlocks;
+ int blockNum = mBlockCacheFirstBlockNumber + i;
+ blocks[index].setStart(blockNum);
+ any = true;
+ }
+ }
+
+ for (int i = 0; i < numBlocks; i++) {
+ int index = (mBlockCacheStartOffset + i) % numBlocks;
+ ImageBlock block = blocks[index];
+ int blockNum = block.mBlockNumber; // mBlockCacheFirstBlockNumber + i;
+ boolean isVis = blockNum >= firstVisBlock && blockNum <= lastVisBlock;
+// Log.v(TAG, "blockNum " + blockNum + " setting vis to " + isVis);
+ block.setVisibility(isVis);
+ }
+
+ if (sDump)
+ mImageBlockManager.dump();
+
+ if (any) {
+ ImageBlockManager.this.notify();
+ mWorkCounter += 1;
+ }
+ }
+ if (sDump)
+ dump();
+ }
+
+ private void check() {
+ ImageBlock [] blocks = mBlockCache;
+ int blockLength = blocks.length;
+
+ // check the results
+ for (int i = 0; i < blockLength; i++) {
+ int index = (mBlockCacheStartOffset + i) % blockLength;
+ if (blocks[index].mBlockNumber != mBlockCacheFirstBlockNumber + i) {
+ if (blocks[index].mBlockNumber != -1)
+ Log.e(TAG, "at " + i + " block cache corrupted; found " + blocks[index].mBlockNumber + " but wanted " + (mBlockCacheFirstBlockNumber + i) + "; offset is " + mBlockCacheStartOffset);
+ }
+ }
+ if (true) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < blockLength; i++) {
+ int index = (mBlockCacheStartOffset + i) % blockLength;
+ ImageBlock b = blocks[index];
+ if (b.mRequestedMask != 0)
+ sb.append("X");
+ else
+ sb.append(String.valueOf(b.mBlockNumber) + " ");
+ }
+ if (Config.LOGV)
+ Log.v(TAG, "moveDataWindow " + sb.toString());
+ }
+ }
+
+ void doDraw(Canvas canvas) {
+ synchronized (ImageBlockManager.this) {
+ ImageBlockManager.ImageBlock [] blocks = mBlockCache;
+ int blockCount = 0;
+
+ if (blocks[0] == null) {
+ return;
+ }
+
+ final int thisHeight = getHeight();
+ final int thisWidth = getWidth();
+ final int height = blocks[0].mBitmap.getHeight();
+ final int scrollPos = mScrollY;
+
+ int currentBlock = (scrollPos < 0) ? ((scrollPos-height+1) / height) : (scrollPos / height);
+
+ while (true) {
+ final int yPos = currentBlock * height;
+ if (yPos >= scrollPos + thisHeight)
+ break;
+
+ if (currentBlock < 0) {
+ canvas.drawRect(0, yPos, thisWidth, 0, mGridViewPaint);
+ currentBlock += 1;
+ continue;
+ }
+ int effectiveOffset = (mBlockCacheStartOffset + (currentBlock++ - mBlockCacheFirstBlockNumber)) % blocks.length;
+ if (effectiveOffset < 0 || effectiveOffset >= blocks.length) {
+ break;
+ }
+
+ ImageBlock block = blocks[effectiveOffset];
+ if (block == null) {
+ break;
+ }
+ synchronized (block) {
+ Bitmap b = block.mBitmap;
+ if (b == null) {
+ break;
+ }
+ canvas.drawBitmap(b, 0, yPos, mGridViewPaint);
+ blockCount += 1;
+ }
+ }
+ }
+ }
+
+ int blockHeight() {
+ return mCurrentSpec.mCellSpacing + mCurrentSpec.mCellHeight;
+ }
+
+ private class ImageBlock {
+ Drawable mCellOutline;
+ Bitmap mBitmap = Bitmap.createBitmap(getWidth(), blockHeight(),
+ Bitmap.Config.RGB_565);;
+ Canvas mCanvas = new Canvas(mBitmap);
+ Paint mPaint = new Paint();
+
+ int mBlockNumber;
+ int mRequestedMask; // columns which have been requested to the loader
+ int mCompletedMask; // columns which have been completed from the loader
+ boolean mIsVisible;
+
+ public void dump(StringBuilder line1, StringBuilder line2) {
+ synchronized (ImageBlock.this) {
+// Log.v(TAG, "block " + mBlockNumber + " isVis == " + mIsVisible);
+ line2.append(mCompletedMask != 0xF ? 'L' : '_');
+ line1.append(mIsVisible ? 'V' : ' ');
+ }
+ }
+
+ ImageBlock() {
+ mPaint.setTextSize(14F);
+ mPaint.setStyle(Paint.Style.FILL);
+
+ mBlockNumber = -1;
+ mCellOutline = GridViewSpecial.this.getResources().getDrawable(android.R.drawable.gallery_thumb);
+ }
+
+ private void recycleBitmaps() {
+ synchronized (ImageBlock.this) {
+ mBitmap.recycle();
+ mBitmap = null;
+ }
+ }
+
+ private void cancelExistingRequests() {
+ synchronized (ImageBlock.this) {
+ for (int i = 0; i < mCurrentSpec.mColumns; i++) {
+ int mask = (1 << i);
+ if ((mRequestedMask & mask) != 0) {
+ int pos = (mBlockNumber * mCurrentSpec.mColumns) + i;
+ if (mLoader.cancel(mGallery.mAllImages.getImageAt(pos))) {
+ mRequestedMask &= ~mask;
+ }
+ }
+ }
+ }
+ }
+
+ private void setStart(final int blockNumber) {
+ synchronized (ImageBlock.this) {
+ if (blockNumber == mBlockNumber)
+ return;
+
+ cancelExistingRequests();
+
+ mBlockNumber = blockNumber;
+ mRequestedMask = 0;
+ mCompletedMask = 0;
+ mCanvas.drawColor(0xFF000000);
+ mPaint.setColor(0xFFDDDDDD);
+ int imageNumber = blockNumber * mCurrentSpec.mColumns;
+ int lastImageNumber = mGallery.mAllImages.getCount() - 1;
+
+ int spacing = mCurrentSpec.mCellSpacing;
+ int leftSpacing = mCurrentSpec.mLeftEdgePadding;
+
+ final int yPos = spacing;
+
+ for (int col = 0; col < mCurrentSpec.mColumns; col++) {
+ if (imageNumber++ >= lastImageNumber)
+ break;
+ final int xPos = leftSpacing + (col * (mCurrentSpec.mCellWidth + spacing));
+ mCanvas.drawRect(xPos, yPos, xPos+mCurrentSpec.mCellWidth, yPos+mCurrentSpec.mCellHeight, mPaint);
+ paintSel(0, xPos, yPos);
+ }
+ }
+ }
+
+ private boolean setVisibility(boolean isVis) {
+ synchronized (ImageBlock.this) {
+ boolean retval = mIsVisible != isVis;
+ mIsVisible = isVis;
+ return retval;
+ }
+ }
+
+ private int startLoading() {
+ synchronized (ImageBlock.this) {
+ final int startRow = mBlockNumber;
+ int count = mGallery.mAllImages.getCount();
+
+ if (startRow == -1)
+ return 0;
+
+ if ((startRow * mCurrentSpec.mColumns) >= count) {
+ return 0;
+ }
+
+ int retVal = 0;
+ int base = (mBlockNumber * mCurrentSpec.mColumns);
+ for (int col = 0; col < mCurrentSpec.mColumns; col++) {
+ if ((mCompletedMask & (1 << col)) != 0) {
+ continue;
+ }
+
+ int spacing = mCurrentSpec.mCellSpacing;
+ int leftSpacing = mCurrentSpec.mLeftEdgePadding;
+ final int yPos = spacing;
+ final int xPos = leftSpacing + (col * (mCurrentSpec.mCellWidth + spacing));
+
+ int pos = base + col;
+ if (pos >= count)
+ break;
+
+ ImageManager.IImage image = mGallery.mAllImages.getImageAt(pos);
+ if (image != null) {
+// Log.v(TAG, "calling loadImage " + (base + col));
+ loadImage(base, col, image, xPos, yPos);
+ retVal += 1;
+ }
+ }
+ return retVal;
+
+ }
+ }
+
+ Bitmap resizeBitmap(Bitmap b) {
+ // assume they're both square for now
+ if (b == null || (b.getWidth() == mCurrentSpec.mCellWidth && b.getHeight() == mCurrentSpec.mCellHeight)) {
+ return b;
+ }
+ float scale = (float) mCurrentSpec.mCellWidth / (float)b.getWidth();
+ Matrix m = new Matrix();
+ m.setScale(scale, scale, b.getWidth(), b.getHeight());
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
+ return b2;
+ }
+
+ private void drawBitmap(ImageManager.IImage image, int base, int baseOffset, Bitmap b, int xPos, int yPos) {
+ mCanvas.setBitmap(mBitmap);
+ if (b != null) {
+ // if the image is close to the target size then crop, otherwise scale
+ // both the bitmap and the view should be square but I suppose that could
+ // change in the future.
+ int w = mCurrentSpec.mCellWidth;
+ int h = mCurrentSpec.mCellHeight;
+
+ int bw = b.getWidth();
+ int bh = b.getHeight();
+
+ int deltaW = bw - w;
+ int deltaH = bh - h;
+
+ if (deltaW < 10 && deltaH < 10) {
+ int halfDeltaW = deltaW / 2;
+ int halfDeltaH = deltaH / 2;
+ android.graphics.Rect src = new android.graphics.Rect(0+halfDeltaW, 0+halfDeltaH, bw-halfDeltaW, bh-halfDeltaH);
+ android.graphics.Rect dst = new android.graphics.Rect(xPos, yPos, xPos+w, yPos+h);
+ if (src.width() != dst.width() || src.height() != dst.height()) {
+ if (Config.LOGV){
+ Log.v(TAG, "nope... width doesn't match " + src.width() + " " + dst.width());
+ Log.v(TAG, "nope... height doesn't match " + src.height() + " " + dst.height());
+ }
+ }
+ mCanvas.drawBitmap(b, src, dst, mPaint);
+ } else {
+ android.graphics.Rect src = new android.graphics.Rect(0, 0, bw, bh);
+ android.graphics.Rect dst = new android.graphics.Rect(xPos, yPos, xPos+w, yPos+h);
+ mCanvas.drawBitmap(b, src, dst, mPaint);
+ }
+ } else {
+ // If the thumbnail cannot be drawn, put up an error icon instead
+ Bitmap error = mImageBlockManager.getErrorBitmap(image);
+ int width = error.getWidth();
+ int height = error.getHeight();
+ Rect source = new Rect(0, 0, width, height);
+ int left = (mCurrentSpec.mCellWidth - width) / 2 + xPos;
+ int top = (mCurrentSpec.mCellHeight - height) / 2 + yPos;
+ Rect dest = new Rect(left, top, left + width, top + height);
+ mCanvas.drawBitmap(error, source, dest, mPaint);
+ }
+ if (ImageManager.isVideo(image)) {
+ Drawable overlay = null;
+ if (MenuHelper.getImageFileSize(image) <= mVideoSizeLimit) {
+ if (mVideoOverlay == null) {
+ mVideoOverlay = getResources().getDrawable(
+ R.drawable.ic_gallery_video_overlay);
+ }
+ overlay = mVideoOverlay;
+ } else {
+ if (mVideoMmsErrorOverlay == null) {
+ mVideoMmsErrorOverlay = getResources().getDrawable(
+ R.drawable.ic_error_mms_video_overlay);
+ }
+ overlay = mVideoMmsErrorOverlay;
+ Paint paint = new Paint();
+ paint.setARGB(0x80, 0x00, 0x00, 0x00);
+ mCanvas.drawRect(xPos, yPos, xPos + mCurrentSpec.mCellWidth,
+ yPos + mCurrentSpec.mCellHeight, paint);
+ }
+ int width = overlay.getIntrinsicWidth();
+ int height = overlay.getIntrinsicHeight();
+ int left = (mCurrentSpec.mCellWidth - width) / 2 + xPos;
+ int top = (mCurrentSpec.mCellHeight - height) / 2 + yPos;
+ Rect newBounds = new Rect(left, top, left + width, top + height);
+ overlay.setBounds(newBounds);
+ overlay.draw(mCanvas);
+ }
+ paintSel(base + baseOffset, xPos, yPos);
+ }
+
+ private void repaintSelection() {
+ int count = mGallery.mAllImages.getCount();
+ int startPos = mBlockNumber * mCurrentSpec.mColumns;
+ synchronized (ImageBlock.this) {
+ for (int i = 0; i < mCurrentSpec.mColumns; i++) {
+ int pos = startPos + i;
+
+ if (pos >= count)
+ break;
+
+ int row = 0; // i / mCurrentSpec.mColumns;
+ int col = i - (row * mCurrentSpec.mColumns);
+
+ // this is duplicated from getOrKick (TODO: don't duplicate this code)
+ int spacing = mCurrentSpec.mCellSpacing;
+ int leftSpacing = mCurrentSpec.mLeftEdgePadding;
+ final int yPos = spacing + (row * (mCurrentSpec.mCellHeight + spacing));
+ final int xPos = leftSpacing + (col * (mCurrentSpec.mCellWidth + spacing));
+
+ paintSel(pos, xPos, yPos);
+ }
+ }
+ }
+
+ private void paintSel(int pos, int xPos, int yPos) {
+ int[] stateSet = EMPTY_STATE_SET;
+ if (pos == mCurrentSelection && mShowSelection) {
+ if (mCurrentSelectionPressed) {
+ stateSet = PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+ } else {
+ stateSet = ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+ }
+ }
+
+ mCellOutline.setState(stateSet);
+ mCanvas.setBitmap(mBitmap);
+ mCellOutline.setBounds(xPos, yPos, xPos+mCurrentSpec.mCellWidth, yPos+mCurrentSpec.mCellHeight);
+ mCellOutline.draw(mCanvas);
+ }
+
+ private void loadImage(
+ final int base,
+ final int baseOffset,
+ final ImageManager.IImage image,
+ final int xPos,
+ final int yPos) {
+ synchronized (ImageBlock.this) {
+ final int startBlock = mBlockNumber;
+ final int pos = base + baseOffset;
+ final ImageLoader.LoadedCallback r = new ImageLoader.LoadedCallback() {
+ public void run(Bitmap b) {
+ boolean more = false;
+ synchronized (ImageBlock.this) {
+ if (startBlock != mBlockNumber) {
+// Log.v(TAG, "wanted block " + mBlockNumber + " but got " + startBlock);
+ return;
+ }
+
+ if (mBitmap == null) {
+ return;
+ }
+
+ drawBitmap(image, base, baseOffset, b, xPos, yPos);
+
+ int mask = (1 << baseOffset);
+ mRequestedMask &= ~mask;
+ mCompletedMask |= mask;
+
+ // Log.v(TAG, "for " + mBlockNumber + " mRequestedMask is " + String.format("%x", mRequestedMask) + " and mCompletedMask is " + String.format("%x", mCompletedMask));
+
+ if (mRequestedMask == 0) {
+ if (mIsVisible) {
+ postInvalidate();
+ }
+ more = true;
+ }
+ }
+ if (b != null)
+ b.recycle();
+
+ if (more) {
+ synchronized (ImageBlockManager.this) {
+ ImageBlockManager.this.notify();
+ mWorkCounter += 1;
+ }
+ }
+ if (sDump)
+ ImageBlockManager.this.dump();
+ }
+ };
+ mRequestedMask |= (1 << baseOffset);
+ mLoader.getBitmap(image, pos, r, mIsVisible, false);
+ }
+ }
+ }
+ }
+
+ public void init(Handler handler) {
+ mHandler = handler;
+ }
+
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (false) {
+ canvas.drawRect(0, 0, getWidth(), getHeight(), mGridViewPaint);
+ if (Config.LOGV)
+ Log.v(TAG, "painting background w/h " + getWidth() + " / " + getHeight());
+ return;
+ }
+
+ if (mImageBlockManager != null) {
+ mImageBlockManager.doDraw(canvas);
+ mImageBlockManager.moveDataWindow(mDirectionBiasDown, false);
+ }
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller != null) {
+ boolean more = mScroller.computeScrollOffset();
+ scrollTo(0, (int)mScroller.getCurrY());
+ if (more) {
+ postInvalidate(); // So we draw again
+ } else {
+ mScroller = null;
+ }
+ } else {
+ super.computeScroll();
+ }
+ }
+
+ private android.graphics.Rect getRectForPosition(int pos) {
+ int row = pos / mCurrentSpec.mColumns;
+ int col = pos - (row * mCurrentSpec.mColumns);
+
+ int left = mCurrentSpec.mLeftEdgePadding + (col * mCurrentSpec.mCellWidth) + (Math.max(0, col-1) * mCurrentSpec.mCellSpacing);
+ int top = (row * mCurrentSpec.mCellHeight) + (row * mCurrentSpec.mCellSpacing);
+
+ return new android.graphics.Rect(left, top, left + mCurrentSpec.mCellWidth + mCurrentSpec.mCellWidth, top + mCurrentSpec.mCellHeight + mCurrentSpec.mCellSpacing);
+ }
+
+ int computeSelectedIndex(android.view.MotionEvent ev) {
+ int spacing = mCurrentSpec.mCellSpacing;
+ int leftSpacing = mCurrentSpec.mLeftEdgePadding;
+
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+ int row = (mScrollY + y - spacing) / (mCurrentSpec.mCellHeight + spacing);
+ int col = Math.min(mCurrentSpec.mColumns - 1, (x - leftSpacing) / (mCurrentSpec.mCellWidth + spacing));
+ return (row * mCurrentSpec.mColumns) + col;
+ }
+
+ @Override
+ public boolean onTouchEvent(android.view.MotionEvent ev) {
+ mGestureDetector.onTouchEvent(ev);
+ return true;
+ }
+
+ private void onSelect(int index) {
+ if (index >= 0 && index < mGallery.mAllImages.getCount()) {
+ ImageManager.IImage img = mGallery.mAllImages.getImageAt(index);
+ if (img == null)
+ return;
+
+ if (mGallery.isPickIntent()) {
+ mGallery.launchCropperOrFinish(img);
+ } else {
+ Uri targetUri = img.fullSizeImageUri();
+ Uri thisUri = mGallery.getIntent().getData();
+ if (thisUri != null) {
+ String bucket = thisUri.getQueryParameter("bucketId");
+ if (bucket != null) {
+ targetUri = targetUri.buildUpon().appendQueryParameter("bucketId", bucket).build();
+ }
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
+
+ if (img instanceof ImageManager.VideoObject) {
+ intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
+ ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ try {
+ mContext.startActivity(intent);
+ } catch (Exception ex) {
+ // sdcard removal??
+ }
+ }
+ }
+ }
+
+ @Override
+ public void scrollBy(int x, int y) {
+ scrollTo(x, mScrollY + y);
+ }
+
+ Toast mDateLocationToast;
+ int [] mDateRange = new int[2];
+
+ private String month(int month) {
+ String text = "";
+ switch (month) {
+ case 0: text = "January"; break;
+ case 1: text = "February"; break;
+ case 2: text = "March"; break;
+ case 3: text = "April"; break;
+ case 4: text = "May"; break;
+ case 5: text = "June"; break;
+ case 6: text = "July"; break;
+ case 7: text = "August"; break;
+ case 8: text = "September"; break;
+ case 9: text = "October"; break;
+ case 10: text = "November"; break;
+ case 11: text = "December"; break;
+ }
+ return text;
+ }
+
+ Runnable mToastRunnable = new Runnable() {
+ public void run() {
+ if (mDateLocationToast != null) {
+ mDateLocationToast.cancel();
+ mDateLocationToast = null;
+ }
+
+ int count = mGallery.mAllImages.getCount();
+ if (count == 0)
+ return;
+
+ GridViewSpecial.this.mImageBlockManager.getVisibleRange(mDateRange);
+
+ ImageManager.IImage firstImage = mGallery.mAllImages.getImageAt(mDateRange[0]);
+ int lastOffset = Math.min(count-1, mDateRange[1]);
+ ImageManager.IImage lastImage = mGallery.mAllImages.getImageAt(lastOffset);
+
+ GregorianCalendar dateStart = new GregorianCalendar();
+ GregorianCalendar dateEnd = new GregorianCalendar();
+
+ dateStart.setTimeInMillis(firstImage.getDateTaken());
+ dateEnd.setTimeInMillis(lastImage.getDateTaken());
+
+ String text1 = month(dateStart.get(Calendar.MONTH)) + " " + dateStart.get(Calendar.YEAR);
+ String text2 = month(dateEnd .get(Calendar.MONTH)) + " " + dateEnd .get(Calendar.YEAR);
+
+ String text = text1;
+ if (!text2.equals(text1))
+ text = text + " : " + text2;
+
+ mDateLocationToast = Toast.makeText(mContext, text, Toast.LENGTH_LONG);
+ mDateLocationToast.show();
+ }
+ };
+
+ @Override
+ public void scrollTo(int x, int y) {
+ y = Math.min(mMaxScrollY, y);
+ y = Math.max(mMinScrollY, y);
+ if (y > mScrollY)
+ mDirectionBiasDown = true;
+ else if (y < mScrollY)
+ mDirectionBiasDown = false;
+ super.scrollTo(x, y);
+ }
+ }
+}
diff --git a/src/com/android/camera/ImageLoader.java b/src/com/android/camera/ImageLoader.java
new file mode 100644
index 0000000..e398fba
--- /dev/null
+++ b/src/com/android/camera/ImageLoader.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2007 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;
+
+import java.util.ArrayList;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.util.Config;
+import android.util.Log;
+
+class ImageLoader {
+ private static final String TAG = "ImageLoader";
+
+ // queue of work to do in the worker thread
+ private ArrayList<WorkItem> mQueue = new ArrayList<WorkItem>();
+ private ArrayList<WorkItem> mInProgress = new ArrayList<WorkItem>();
+
+ // the worker thread and a done flag so we know when to exit
+ // currently we only exit from finalize
+ private boolean mDone;
+ private ArrayList<Thread> mDecodeThreads = new ArrayList<Thread>();
+ private android.os.Handler mHandler;
+
+ private int mThreadCount = 1;
+
+ synchronized void clear(Uri uri) {
+ }
+
+ synchronized public void dump() {
+ synchronized (mQueue) {
+ if (Config.LOGV)
+ Log.v(TAG, "Loader queue length is " + mQueue.size());
+ }
+ }
+
+ public interface LoadedCallback {
+ public void run(Bitmap result);
+ }
+
+ public void pushToFront(final ImageManager.IImage image) {
+ synchronized (mQueue) {
+ WorkItem w = new WorkItem(image, 0, null, false);
+
+ int existing = mQueue.indexOf(w);
+ if (existing >= 1) {
+ WorkItem existingWorkItem = mQueue.remove(existing);
+ mQueue.add(0, existingWorkItem);
+ mQueue.notifyAll();
+ }
+ }
+ }
+
+ public boolean cancel(final ImageManager.IImage image) {
+ synchronized (mQueue) {
+ WorkItem w = new WorkItem(image, 0, null, false);
+
+ int existing = mQueue.indexOf(w);
+ if (existing >= 0) {
+ mQueue.remove(existing);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ public Bitmap getBitmap(final ImageManager.IImage image, final LoadedCallback imageLoadedRunnable, final boolean postAtFront, boolean postBack) {
+ return getBitmap(image, 0, imageLoadedRunnable, postAtFront, postBack);
+ }
+
+ public Bitmap getBitmap(final ImageManager.IImage image, int tag, final LoadedCallback imageLoadedRunnable, final boolean postAtFront, boolean postBack) {
+ synchronized (mDecodeThreads) {
+ if (mDecodeThreads.size() == 0) {
+ start();
+ }
+ }
+ long t1 = System.currentTimeMillis();
+ long t2,t3,t4;
+ synchronized (mQueue) {
+ t2 = System.currentTimeMillis();
+ WorkItem w = new WorkItem(image, tag, imageLoadedRunnable, postBack);
+
+ if (!mInProgress.contains(w)) {
+ boolean contains = mQueue.contains(w);
+ if (contains) {
+ if (postAtFront) {
+ // move this item to the front
+ mQueue.remove(w);
+ mQueue.add(0, w);
+ }
+ } else {
+ if (postAtFront)
+ mQueue.add(0, w);
+ else
+ mQueue.add(w);
+ mQueue.notifyAll();
+ }
+ }
+ if (false)
+ dumpQueue("+" + (postAtFront ? "F " : "B ") + tag + ": ");
+ t3 = System.currentTimeMillis();
+ }
+ t4 = System.currentTimeMillis();
+// Log.v(TAG, "getBitmap breakdown: tot= " + (t4-t1) + "; " + "; " + (t4-t3) + "; " + (t3-t2) + "; " + (t2-t1));
+ return null;
+ }
+
+ private void dumpQueue(String s) {
+ synchronized (mQueue) {
+ StringBuilder sb = new StringBuilder(s);
+ for (int i = 0; i < mQueue.size(); i++) {
+ sb.append(mQueue.get(i).mTag + " ");
+ }
+ if (Config.LOGV)
+ Log.v(TAG, sb.toString());
+ }
+ }
+
+ long bitmapSize(Bitmap b) {
+ return b.getWidth() * b.getHeight() * 4;
+ }
+
+ class WorkItem {
+ ImageManager.IImage mImage;
+ int mTargetX, mTargetY;
+ int mTag;
+ LoadedCallback mOnLoadedRunnable;
+ boolean mPostBack;
+
+ WorkItem(ImageManager.IImage image, int tag, LoadedCallback onLoadedRunnable, boolean postBack) {
+ mImage = image;
+ mTag = tag;
+ mOnLoadedRunnable = onLoadedRunnable;
+ mPostBack = postBack;
+ }
+
+ public boolean equals(Object other) {
+ WorkItem otherWorkItem = (WorkItem) other;
+ if (otherWorkItem.mImage != mImage)
+ return false;
+
+ return true;
+ }
+
+ public int hashCode() {
+ return mImage.fullSizeImageUri().hashCode();
+ }
+ }
+
+ public ImageLoader(android.os.Handler handler, int threadCount) {
+ mThreadCount = threadCount;
+ mHandler = handler;
+ start();
+ }
+
+ synchronized private void start() {
+ if (Config.LOGV)
+ Log.v(TAG, "ImageLoader.start() <<<<<<<<<<<<<<<<<<<<<<<<<<<<");
+
+ synchronized (mDecodeThreads) {
+ if (mDecodeThreads.size() > 0)
+ return;
+
+ mDone = false;
+ for (int i = 0;i < mThreadCount; i++) {
+ Thread t = new Thread(new Runnable() {
+ // pick off items on the queue, one by one, and compute their bitmap.
+ // place the resulting bitmap in the cache. then post a notification
+ // back to the ui so things can get updated appropriately.
+ public void run() {
+ while (!mDone) {
+ WorkItem workItem = null;
+ synchronized (mQueue) {
+ if (mQueue.size() > 0) {
+ workItem = mQueue.remove(0);
+ mInProgress.add(workItem);
+ }
+ else {
+ try {
+ mQueue.wait();
+ } catch (InterruptedException ex) {
+ }
+ }
+ }
+ if (workItem != null) {
+ if (false)
+ dumpQueue("-" + workItem.mTag + ": ");
+ Bitmap b = null;
+ try {
+ b = workItem.mImage.miniThumbBitmap();
+ } catch (Exception ex) {
+ if (Config.LOGV) Log.v(TAG, "couldn't load miniThumbBitmap " + ex.toString());
+ // sd card removal or sd card full
+ }
+ if (b == null) {
+ if (Config.LOGV) Log.v(TAG, "unable to read thumbnail for " + workItem.mImage.fullSizeImageUri());
+ }
+
+ synchronized (mQueue) {
+ mInProgress.remove(workItem);
+ }
+
+ if (workItem.mOnLoadedRunnable != null) {
+ if (workItem.mPostBack) {
+ final WorkItem w1 = workItem;
+ final Bitmap bitmap = b;
+ if (!mDone) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ w1.mOnLoadedRunnable.run(bitmap);
+ }
+ });
+ }
+ } else {
+ workItem.mOnLoadedRunnable.run(b);
+ }
+ }
+ }
+ }
+ }
+ });
+ t.setName("image-loader-" + i);
+ mDecodeThreads.add(t);
+ t.start();
+ }
+ }
+ }
+
+ public static Bitmap transform(Matrix scaler, Bitmap source, int targetWidth, int targetHeight,
+ boolean scaleUp) {
+ int deltaX = source.getWidth() - targetWidth;
+ int deltaY = source.getHeight() - targetHeight;
+ if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
+ /*
+ * In this case the bitmap is smaller, at least in one dimension, than the
+ * target. Transform it by placing as much of the image as possible into
+ * the target and leaving the top/bottom or left/right (or both) black.
+ */
+ Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(b2);
+
+ int deltaXHalf = Math.max(0, deltaX/2);
+ int deltaYHalf = Math.max(0, deltaY/2);
+ Rect src = new Rect(
+ deltaXHalf,
+ deltaYHalf,
+ deltaXHalf + Math.min(targetWidth, source.getWidth()),
+ deltaYHalf + Math.min(targetHeight, source.getHeight()));
+ int dstX = (targetWidth - src.width()) / 2;
+ int dstY = (targetHeight - src.height()) / 2;
+ Rect dst = new Rect(
+ dstX,
+ dstY,
+ targetWidth - dstX,
+ targetHeight - dstY);
+ if (Config.LOGV)
+ Log.v(TAG, "draw " + src.toString() + " ==> " + dst.toString());
+ c.drawBitmap(source, src, dst, null);
+ return b2;
+ }
+ float bitmapWidthF = source.getWidth();
+ float bitmapHeightF = source.getHeight();
+
+ float bitmapAspect = bitmapWidthF / bitmapHeightF;
+ float viewAspect = (float) targetWidth / (float) targetHeight;
+
+ if (bitmapAspect > viewAspect) {
+ float scale = targetHeight / bitmapHeightF;
+ if (scale < .9F || scale > 1F) {
+ scaler.setScale(scale, scale);
+ } else {
+ scaler = null;
+ }
+ } else {
+ float scale = targetWidth / bitmapWidthF;
+ if (scale < .9F || scale > 1F) {
+ scaler.setScale(scale, scale);
+ } else {
+ scaler = null;
+ }
+ }
+
+ Bitmap b1;
+ if (scaler != null) {
+ // this is used for minithumb and crop, so we want to filter here.
+ b1 = Bitmap.createBitmap(source, 0, 0,
+ source.getWidth(), source.getHeight(), scaler, true);
+ } else {
+ b1 = source;
+ }
+
+ int dx1 = Math.max(0, b1.getWidth() - targetWidth);
+ int dy1 = Math.max(0, b1.getHeight() - targetHeight);
+
+ Bitmap b2 = Bitmap.createBitmap(
+ b1,
+ dx1/2,
+ dy1/2,
+ targetWidth,
+ targetHeight);
+
+ if (b1 != source)
+ b1.recycle();
+
+ return b2;
+ }
+
+ public void stop() {
+ if (Config.LOGV)
+ Log.v(TAG, "ImageLoader.stop " + mDecodeThreads.size() + " threads");
+ mDone = true;
+ synchronized (mQueue) {
+ mQueue.notifyAll();
+ }
+ while (mDecodeThreads.size() > 0) {
+ Thread t = mDecodeThreads.get(0);
+ try {
+ t.join();
+ mDecodeThreads.remove(0);
+ } catch (InterruptedException ex) {
+ // so now what?
+ }
+ }
+ }
+}
diff --git a/src/com/android/camera/ImageManager.java b/src/com/android/camera/ImageManager.java
new file mode 100755
index 0000000..99e5366
--- /dev/null
+++ b/src/com/android/camera/ImageManager.java
@@ -0,0 +1,4203 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.ContentUris;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.location.Location;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.provider.DrmStore;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Images.Thumbnails;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.MediaColumns;
+import android.provider.MediaStore.Video;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ *
+ * ImageManager is used to retrieve and store images
+ * in the media content provider.
+ *
+ */
+public class ImageManager {
+ public static final String CAMERA_IMAGE_BUCKET_NAME =
+ Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera";
+ public static final String CAMERA_IMAGE_BUCKET_ID = getBucketId(CAMERA_IMAGE_BUCKET_NAME);
+
+ /**
+ * Matches code in MediaProvider.computeBucketValues. Should be a common function.
+ */
+
+ public static String getBucketId(String path) {
+ return String.valueOf(path.toLowerCase().hashCode());
+ }
+
+ /**
+ * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be imported.
+ * This is a temporary fix for bug#1655552.
+ */
+ public static void ensureOSXCompatibleFolder() {
+ File nnnAAAAA = new File(
+ Environment.getExternalStorageDirectory().toString() + "/DCIM/100ANDRO");
+ if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) {
+ Log.e(TAG, "create NNNAAAAA file: "+ nnnAAAAA.getPath()+" failed");
+ }
+ }
+
+ // To enable verbose logging for this class, change false to true. The other logic ensures that
+ // this logging can be disabled by turned off DEBUG and lower, and that it can be enabled by
+ // "setprop log.tag.ImageManager VERBOSE" if desired.
+ //
+ // IMPORTANT: Never check in this file set to true!
+ private static final boolean VERBOSE = Config.LOGD && (false || Config.LOGV);
+ private static final String TAG = "ImageManager";
+
+ private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
+
+ static public void debug_where(String tag, String msg) {
+ try {
+ throw new Exception();
+ } catch (Exception ex) {
+ if (msg != null) {
+ Log.v(tag, msg);
+ }
+ boolean first = true;
+ for (StackTraceElement s : ex.getStackTrace()) {
+ if (first)
+ first = false;
+ else
+ Log.v(tag, s.toString());
+ }
+ }
+ }
+
+ /*
+ * Compute the sample size as a function of the image size and the target.
+ * Scale the image down so that both the width and height are just above
+ * the target. If this means that one of the dimension goes from above
+ * the target to below the target (e.g. given a width of 480 and an image
+ * width of 600 but sample size of 2 -- i.e. new width 300 -- bump the
+ * sample size down by 1.
+ */
+ private static int computeSampleSize(BitmapFactory.Options options, int target) {
+ int w = options.outWidth;
+ int h = options.outHeight;
+
+ int candidateW = w / target;
+ int candidateH = h / target;
+ int candidate = Math.max(candidateW, candidateH);
+
+ if (candidate == 0)
+ return 1;
+
+ if (candidate > 1) {
+ if ((w > target) && (w / candidate) < target)
+ candidate -= 1;
+ }
+
+ if (candidate > 1) {
+ if ((h > target) && (h / candidate) < target)
+ candidate -= 1;
+ }
+
+ if (VERBOSE)
+ Log.v(TAG, "for w/h " + w + "/" + h + " returning " + candidate + "(" + (w/candidate) + " / " + (h/candidate));
+
+ return candidate;
+ }
+ /*
+ * All implementors of ICancelable should inherit from BaseCancelable
+ * since it provides some convenience methods such as acknowledgeCancel
+ * and checkCancel.
+ */
+ public abstract class BaseCancelable implements ICancelable {
+ boolean mCancel = false;
+ boolean mFinished = false;
+
+ /*
+ * Subclasses should call acknowledgeCancel when they're finished with
+ * their operation.
+ */
+ protected void acknowledgeCancel() {
+ synchronized (this) {
+ mFinished = true;
+ if (!mCancel)
+ return;
+ if (mCancel) {
+ this.notify();
+ }
+ }
+ }
+
+ public boolean cancel() {
+ synchronized (this) {
+ if (mCancel) {
+ return false;
+ }
+ if (mFinished) {
+ return false;
+ }
+ mCancel = true;
+ boolean retVal = doCancelWork();
+
+ try {
+ this.wait();
+ } catch (InterruptedException ex) {
+ // now what??? TODO
+ }
+
+ return retVal;
+ }
+ }
+
+ /*
+ * Subclasses can call this to see if they have been canceled.
+ * This is the polling model.
+ */
+ protected void checkCanceled() throws CanceledException {
+ synchronized (this) {
+ if (mCancel)
+ throw new CanceledException();
+ }
+ }
+
+ /*
+ * Subclasses implement this method to take whatever action
+ * is necessary when getting canceled. Sometimes it's not
+ * possible to do anything in which case the "checkCanceled"
+ * polling model may be used (or some combination).
+ */
+ public abstract boolean doCancelWork();
+ }
+
+ private static final int sBytesPerMiniThumb = 10000;
+ static final private byte [] sMiniThumbData = new byte[sBytesPerMiniThumb];
+
+ /**
+ * Represents a particular image and provides access
+ * to the underlying bitmap and two thumbnail bitmaps
+ * as well as other information such as the id, and
+ * the path to the actual image data.
+ */
+ abstract class BaseImage implements IImage {
+ protected ContentResolver mContentResolver;
+ protected long mId, mMiniThumbMagic;
+ protected BaseImageList mContainer;
+ protected HashMap<String, String> mExifData;
+ protected int mCursorRow;
+
+ protected BaseImage(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow) {
+ mContentResolver = cr;
+ mId = id;
+ mMiniThumbMagic = miniThumbId;
+ mContainer = container;
+ mCursorRow = cursorRow;
+ }
+
+ abstract Bitmap.CompressFormat compressionType();
+
+ public void commitChanges() {
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.commitUpdates();
+ c.requery();
+ }
+ }
+ }
+
+ /**
+ * Take a given bitmap and compress it to a file as described
+ * by the Uri parameter.
+ *
+ * @param bitmap the bitmap to be compressed/stored
+ * @param uri where to store the bitmap
+ * @return true if we succeeded
+ */
+ protected IGetBoolean_cancelable compressImageToFile(
+ final Bitmap bitmap,
+ final byte [] jpegData,
+ final Uri uri) {
+ class CompressImageToFile extends BaseCancelable implements IGetBoolean_cancelable {
+ ThreadSafeOutputStream mOutputStream = null;
+
+ public boolean doCancelWork() {
+ if (mOutputStream != null) {
+ try {
+ mOutputStream.close();
+ return true;
+ } catch (IOException ex) {
+ // TODO what to do here
+ }
+ }
+ return false;
+ }
+
+ public boolean get() {
+ try {
+ long t1 = System.currentTimeMillis();
+ OutputStream delegate = mContentResolver.openOutputStream(uri);
+ synchronized (this) {
+ checkCanceled();
+ mOutputStream = new ThreadSafeOutputStream(delegate);
+ }
+ long t2 = System.currentTimeMillis();
+ if (bitmap != null) {
+ bitmap.compress(compressionType(), 75, mOutputStream);
+ } else {
+ long x1 = System.currentTimeMillis();
+ mOutputStream.write(jpegData);
+ long x2 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "done writing... " + jpegData.length + " bytes took " + (x2-x1));
+ }
+ long t3 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, String.format("CompressImageToFile.get took %d (%d, %d)",(t3-t1),(t2-t1),(t3-t2)));
+ return true;
+ } catch (FileNotFoundException ex) {
+ return false;
+ } catch (CanceledException ex) {
+ return false;
+ } catch (IOException ex) {
+ return false;
+ }
+ finally {
+ if (mOutputStream != null) {
+ try {
+ mOutputStream.close();
+ } catch (IOException ex) {
+ // not much we can do here so ignore
+ }
+ }
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new CompressImageToFile();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null)
+ return false;
+ if (!(other instanceof Image))
+ return false;
+
+ return fullSizeImageUri().equals(((Image)other).fullSizeImageUri());
+ }
+
+ public Bitmap fullSizeBitmap(int targetWidthHeight) {
+ return fullSizeBitmap(targetWidthHeight, true);
+ }
+
+ protected Bitmap fullSizeBitmap(int targetWidthHeight, boolean rotateAsNeeded) {
+ Uri url = mContainer.contentUri(mId);
+ if (VERBOSE) Log.v(TAG, "getCreateBitmap for " + url);
+ if (url == null)
+ return null;
+
+ Bitmap b = null;
+ if (b == null) {
+ b = makeBitmap(targetWidthHeight, url);
+ if (b != null && rotateAsNeeded) {
+ b = rotate(b, getDegreesRotated());
+ }
+ }
+ return b;
+ }
+
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthHeight) {
+ final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable {
+ ParcelFileDescriptor mPFD;
+ BitmapFactory.Options mOptions = new BitmapFactory.Options();
+ long mCancelInitiationTime;
+
+ public LoadBitmapCancelable(ParcelFileDescriptor pfdInput) {
+ mPFD = pfdInput;
+ }
+
+ public boolean doCancelWork() {
+ if (VERBOSE)
+ Log.v(TAG, "requesting bitmap load cancel");
+ mCancelInitiationTime = System.currentTimeMillis();
+ mOptions.requestCancelDecode();
+ return true;
+ }
+
+ public Bitmap get() {
+ try {
+ Bitmap b = makeBitmap(targetWidthHeight, fullSizeImageUri(), mPFD, mOptions);
+ if (mCancelInitiationTime != 0) {
+ if (VERBOSE)
+ Log.v(TAG, "cancelation of bitmap load success==" + (b == null ? "TRUE" : "FALSE") + " -- took " + (System.currentTimeMillis() - mCancelInitiationTime));
+ }
+ if (b != null) {
+ int degrees = getDegreesRotated();
+ if (degrees != 0) {
+ Matrix m = new Matrix();
+ m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ if (b != b2)
+ b.recycle();
+ b = b2;
+ }
+ }
+ return b;
+ } catch (Exception ex) {
+ return null;
+ } finally {
+ acknowledgeCancel();
+ }
+ }
+ }
+
+ try {
+ ParcelFileDescriptor pfdInput = mContentResolver.openFileDescriptor(fullSizeImageUri(), "r");
+ return new LoadBitmapCancelable(pfdInput);
+ } catch (FileNotFoundException ex) {
+ return null;
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+ }
+
+ public InputStream fullSizeImageData() {
+ try {
+ InputStream input = mContentResolver.openInputStream(
+ fullSizeImageUri());
+ return input;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ public long fullSizeImageId() {
+ return mId;
+ }
+
+ public Uri fullSizeImageUri() {
+ return mContainer.contentUri(mId);
+ }
+
+ public IImageList getContainer() {
+ return mContainer;
+ }
+
+ Cursor getCursor() {
+ return mContainer.getCursor();
+ }
+
+ public long getDateTaken() {
+ if (mContainer.indexDateTaken() < 0) return 0;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getLong(mContainer.indexDateTaken());
+ }
+ }
+
+ protected int getDegreesRotated() {
+ return 0;
+ }
+
+ public String getMimeType() {
+ if (mContainer.indexMimeType() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.MIME_TYPE },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ } else {
+ return "";
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String mimeType = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ mimeType = c.getString(mContainer.indexMimeType());
+ }
+ }
+ return mimeType;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getDescription()
+ */
+ public String getDescription() {
+ if (mContainer.indexDescription() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.DESCRIPTION },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ } else {
+ return "";
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String description = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ description = c.getString(mContainer.indexDescription());
+ }
+ }
+ return description;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getIsPrivate()
+ */
+ public boolean getIsPrivate() {
+ if (mContainer.indexPrivate() < 0) return false;
+ boolean isPrivate = false;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ isPrivate = c.getInt(mContainer.indexPrivate()) != 0;
+ }
+ }
+ return isPrivate;
+ }
+
+ public double getLatitude() {
+ if (mContainer.indexLatitude() < 0) return 0D;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getDouble(mContainer.indexLatitude());
+ }
+ }
+
+ public double getLongitude() {
+ if (mContainer.indexLongitude() < 0) return 0D;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getDouble(mContainer.indexLongitude());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getTitle()
+ */
+ public String getTitle() {
+ String name = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ if (mContainer.indexTitle() != -1) {
+ name = c.getString(mContainer.indexTitle());
+ }
+ }
+ }
+ return name != null && name.length() > 0 ? name : String.valueOf(mId);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getDisplayName()
+ */
+ public String getDisplayName() {
+ if (mContainer.indexDisplayName() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.DISPLAY_NAME },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String name = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ name = c.getString(mContainer.indexDisplayName());
+ }
+ }
+ if (name != null && name.length() > 0)
+ return name;
+ }
+ return String.valueOf(mId);
+ }
+
+ public String getPicasaId() {
+ /*
+ if (mContainer.indexPicasaWeb() < 0) return null;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveTo(getRow());
+ return c.getString(mContainer.indexPicasaWeb());
+ }
+ */
+ return null;
+ }
+
+ public int getRow() {
+ return mCursorRow;
+ }
+
+ public int getWidth() {
+ ParcelFileDescriptor input = null;
+ try {
+ Uri uri = fullSizeImageUri();
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options.outWidth;
+ } catch (IOException ex) {
+ return 0;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ public int getHeight() {
+ ParcelFileDescriptor input = null;
+ try {
+ Uri uri = fullSizeImageUri();
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options.outHeight;
+ } catch (IOException ex) {
+ return 0;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ public boolean hasLatLong() {
+ if (mContainer.indexLatitude() < 0 || mContainer.indexLongitude() < 0) return false;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return !c.isNull(mContainer.indexLatitude()) && !c.isNull(mContainer.indexLongitude());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#imageId()
+ */
+ public long imageId() {
+ return mId;
+ }
+
+ /**
+ * Make a bitmap from a given Uri.
+ *
+ * @param uri
+ */
+ private Bitmap makeBitmap(int targetWidthOrHeight, Uri uri) {
+ ParcelFileDescriptor input = null;
+ try {
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ return makeBitmap(targetWidthOrHeight, uri, input, null);
+ } catch (IOException ex) {
+ return null;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) {
+ return mContainer.makeBitmap(targetWidthHeight, uri, pfdInput, options);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#thumb1()
+ */
+ public Bitmap miniThumbBitmap() {
+ try {
+ long id = mId;
+ long dbMagic = mMiniThumbMagic;
+ if (dbMagic == 0 || dbMagic == id) {
+ dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow());
+ if (VERBOSE) Log.v(TAG, "after computing thumbnail dbMagic is " + dbMagic);
+ }
+
+ synchronized(sMiniThumbData) {
+ dbMagic = mMiniThumbMagic;
+ byte [] data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
+ if (data == null) {
+ byte[][] createdThumbData = new byte[1][];
+ try {
+ dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(),
+ getRow(), createdThumbData);
+ } catch (IOException ex) {
+ // Typically IOException because the sd card is full.
+ // But createdThumbData may have been filled in, so continue on.
+ }
+ data = createdThumbData[0];
+ }
+ if (data == null) {
+ data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
+ }
+ if (data == null) {
+ if (VERBOSE)
+ Log.v(TAG, "unable to get miniThumbBitmap, data is null");
+ }
+ if (data != null) {
+ Bitmap b = BitmapFactory.decodeByteArray(data, 0, data.length);
+ if (b == null) {
+ if (VERBOSE) {
+ Log.v(TAG, "couldn't decode byte array for mini thumb, length was " + data.length);
+ }
+ }
+ return b;
+ }
+ }
+ return null;
+ } catch (Exception ex) {
+ // Typically IOException because the sd card is full.
+ if (VERBOSE) {
+ Log.e(TAG, "miniThumbBitmap got exception " + ex.toString());
+ for (StackTraceElement s : ex.getStackTrace())
+ Log.e(TAG, "... " + s.toString());
+ }
+ return null;
+ }
+ }
+
+ public void onRemove() {
+ mContainer.mCache.remove(mId);
+ }
+
+ protected void saveMiniThumb(Bitmap source) throws IOException {
+ mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setName()
+ */
+ public void setDescription(String description) {
+ if (mContainer.indexDescription() < 0) return;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateString(mContainer.indexDescription(), description);
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setIsPrivate()
+ */
+ public void setIsPrivate(boolean isPrivate) {
+ if (mContainer.indexPrivate() < 0) return;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateInt(mContainer.indexPrivate(), isPrivate ? 1 : 0);
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setName()
+ */
+ public void setName(String name) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateString(mContainer.indexTitle(), name);
+ }
+ }
+ }
+
+ public void setPicasaId(String id) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.PICASA_ID },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ if (VERBOSE) {
+ Log.v(TAG, "storing picasaid " + id + " for " + fullSizeImageUri());
+ }
+ c.updateString(1, id);
+ c.commitUpdates();
+ if (VERBOSE) {
+ Log.v(TAG, "updated image with picasa id " + id);
+ }
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#thumbUri()
+ */
+ public Uri thumbUri() {
+ Uri uri = fullSizeImageUri();
+ // The value for the query parameter cannot be null :-(, so using a dummy "1"
+ uri = uri.buildUpon().appendQueryParameter("thumb", "1").build();
+ return uri;
+ }
+
+ @Override
+ public String toString() {
+ return fullSizeImageUri().toString();
+ }
+ }
+
+ abstract static class BaseImageList implements IImageList {
+ Context mContext;
+ ContentResolver mContentResolver;
+ Uri mBaseUri, mUri;
+ int mSort;
+ String mBucketId;
+ boolean mDistinct;
+ Cursor mCursor;
+ boolean mCursorDeactivated;
+ protected HashMap<Long, IImage> mCache = new HashMap<Long, IImage>();
+
+ IImageList.OnChange mListener = null;
+ Handler mHandler;
+ protected RandomAccessFile mMiniThumbData;
+ protected Uri mThumbUri;
+
+ public BaseImageList(Context ctx, ContentResolver cr, Uri uri, int sort, String bucketId) {
+ mContext = ctx;
+ mSort = sort;
+ mUri = uri;
+ mBaseUri = uri;
+ mBucketId = bucketId;
+
+ mContentResolver = cr;
+ }
+
+ String randomAccessFilePath(int version) {
+ String directoryName = Environment.getExternalStorageDirectory().toString() + "/DCIM/.thumbnails";
+ String path = directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
+ return path;
+ }
+
+ RandomAccessFile miniThumbDataFile() {
+ if (mMiniThumbData == null) {
+ String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
+ File directory = new File(new File(path).getParent());
+ if (!directory.isDirectory()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "!!!! unable to create .thumbnails directory " + directory.toString());
+ }
+ }
+ File f = new File(path);
+ if (VERBOSE) Log.v(TAG, "file f is " + f.toString());
+ try {
+ mMiniThumbData = new RandomAccessFile(f, "rw");
+ } catch (IOException ex) {
+
+ }
+ }
+ return mMiniThumbData;
+ }
+
+ /**
+ * Store a given thumbnail in the database.
+ */
+ protected Bitmap storeThumbnail(Bitmap thumb, long imageId) {
+ if (thumb == null)
+ return null;
+
+ try {
+ Uri uri = getThumbnailUri(imageId, thumb.getWidth(), thumb.getHeight());
+ if (uri == null) {
+ return thumb;
+ }
+ OutputStream thumbOut = mContentResolver.openOutputStream(uri);
+ thumb.compress(Bitmap.CompressFormat.JPEG, 60, thumbOut);
+ thumbOut.close();
+ return thumb;
+ }
+ catch (Exception ex) {
+ if (VERBOSE) Log.d(TAG, "unable to store thumbnail: " + ex);
+ return thumb;
+ }
+ }
+
+ /**
+ * Store a JPEG thumbnail from the EXIF header in the database.
+ */
+ protected boolean storeThumbnail(byte[] jpegThumbnail, long imageId, int width, int height) {
+ if (jpegThumbnail == null)
+ return false;
+
+ Uri uri = getThumbnailUri(imageId, width, height);
+ if (uri == null) {
+ return false;
+ }
+ try {
+ OutputStream thumbOut = mContentResolver.openOutputStream(uri);
+ thumbOut.write(jpegThumbnail);
+ thumbOut.close();
+ return true;
+ }
+ catch (FileNotFoundException ex) {
+ return false;
+ }
+ catch (IOException ex) {
+ return false;
+ }
+ }
+
+ private Uri getThumbnailUri(long imageId, int width, int height) {
+ // we do not store thumbnails for DRM'd images
+ if (mThumbUri == null) {
+ return null;
+ }
+
+ Uri uri = null;
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ mThumbUri,
+ THUMB_PROJECTION,
+ Thumbnails.IMAGE_ID + "=?",
+ new String[]{String.valueOf(imageId)},
+ null);
+ if (c != null && c.moveToFirst()) {
+ // If, for some reaosn, we already have a row with a matching
+ // image id, then just update that row rather than creating a
+ // new row.
+ uri = ContentUris.withAppendedId(mThumbUri, c.getLong(indexThumbId()));
+ c.commitUpdates();
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ if (uri == null) {
+ ContentValues values = new ContentValues(4);
+ values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND);
+ values.put(Images.Thumbnails.IMAGE_ID, imageId);
+ values.put(Images.Thumbnails.HEIGHT, height);
+ values.put(Images.Thumbnails.WIDTH, width);
+ uri = mContentResolver.insert(mThumbUri, values);
+ }
+ return uri;
+ }
+
+ java.util.Random mRandom = new java.util.Random(System.currentTimeMillis());
+
+ protected SomewhatFairLock mLock = new SomewhatFairLock();
+
+ class SomewhatFairLock {
+ private Object mSync = new Object();
+ private boolean mLocked = false;
+ private ArrayList<Thread> mWaiting = new ArrayList<Thread>();
+
+ void lock() {
+// if (VERBOSE) Log.v(TAG, "lock... thread " + Thread.currentThread().getId());
+ synchronized (mSync) {
+ while (mLocked) {
+ try {
+// if (VERBOSE) Log.v(TAG, "waiting... thread " + Thread.currentThread().getId());
+ mWaiting.add(Thread.currentThread());
+ mSync.wait();
+ if (mWaiting.get(0) == Thread.currentThread()) {
+ mWaiting.remove(0);
+ break;
+ }
+ } catch (InterruptedException ex) {
+ //
+ }
+ }
+// if (VERBOSE) Log.v(TAG, "locked... thread " + Thread.currentThread().getId());
+ mLocked = true;
+ }
+ }
+
+ void unlock() {
+// if (VERBOSE) Log.v(TAG, "unlocking... thread " + Thread.currentThread().getId());
+ synchronized (mSync) {
+ mLocked = false;
+ mSync.notifyAll();
+ }
+ }
+ }
+
+ // If the photo has an EXIF thumbnail and it's big enough, extract it and save that JPEG as
+ // the large thumbnail without re-encoding it. We still have to decompress it though, in
+ // order to generate the minithumb.
+ private Bitmap createThumbnailFromEXIF(String filePath, long id) {
+ if (filePath != null) {
+ byte [] thumbData = null;
+ synchronized (ImageManager.instance()) {
+ thumbData = (new ExifInterface(filePath)).getThumbnail();
+ }
+ if (thumbData != null) {
+ // Sniff the size of the EXIF thumbnail before decoding it. Photos from the
+ // device will pass, but images that are side loaded from other cameras may not.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
+ int width = options.outWidth;
+ int height = options.outHeight;
+ if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE) {
+ if (storeThumbnail(thumbData, id, width, height)) {
+ // this is used for *encoding* the minithumb, so
+ // we don't want to dither or convert to 565 here.
+ //
+ // Decode with a scaling factor
+ // to match MINI_THUMB_TARGET_SIZE closely
+ // which will produce much better scaling quality
+ // and is significantly faster.
+ options.inSampleSize = computeSampleSize(options, THUMBNAIL_TARGET_SIZE);
+
+ if (VERBOSE) {
+ Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + options.inSampleSize);
+ }
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ // The fallback case is to decode the original photo to thumbnail size, then encode it as a
+ // JPEG. We return the thumbnail Bitmap in order to create the minithumb from it.
+ private Bitmap createThumbnailFromUri(Cursor c, long id) {
+ Uri uri = ContentUris.withAppendedId(mBaseUri, id);
+ Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null);
+ if (bitmap != null) {
+ storeThumbnail(bitmap, id);
+ } else {
+ uri = ContentUris.withAppendedId(mBaseUri, id);
+ bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null);
+ }
+ return bitmap;
+ }
+
+ // returns id
+ public long checkThumbnail(BaseImage existingImage, Cursor c, int i) throws IOException {
+ return checkThumbnail(existingImage, c, i, null);
+ }
+
+ /**
+ * Checks to see if a mini thumbnail exists in the cache. If not, tries to create it and
+ * add it to the cache.
+ * @param existingImage
+ * @param c
+ * @param i
+ * @param createdThumbnailData if this parameter is non-null, and a new mini-thumbnail
+ * bitmap is created, the new bitmap's data will be stored in createdThumbnailData[0].
+ * Note that if the sdcard is full, it's possible that
+ * createdThumbnailData[0] will be set even if the method throws an IOException. This is
+ * actually useful, because it allows the caller to use the created thumbnail even if
+ * the sdcard is full.
+ * @return
+ * @throws IOException
+ */
+ public long checkThumbnail(BaseImage existingImage, Cursor c, int i,
+ byte[][] createdThumbnailData) throws IOException {
+ long magic, fileMagic = 0, id;
+ try {
+ mLock.lock();
+ if (existingImage == null) {
+ // if we don't have an Image object then get the id and magic from
+ // the cursor. Synchronize on the cursor object.
+ synchronized (c) {
+ if (!c.moveToPosition(i)) {
+ return -1;
+ }
+ magic = c.getLong(indexMiniThumbId());
+ id = c.getLong(indexId());
+ }
+ } else {
+ // if we have an Image object then ask them for the magic/id
+ magic = existingImage.mMiniThumbMagic;
+ id = existingImage.fullSizeImageId();
+ }
+
+ if (magic != 0) {
+ // check the mini thumb file for the right data. Right is defined as
+ // having the right magic number at the offset reserved for this "id".
+ RandomAccessFile r = miniThumbDataFile();
+ if (r != null) {
+ synchronized (r) {
+ long pos = id * sBytesPerMiniThumb;
+ try {
+ // check that we can read the following 9 bytes (1 for the "status" and 8 for the long)
+ if (r.length() >= pos + 1 + 8) {
+ r.seek(pos);
+ if (r.readByte() == 1) {
+ fileMagic = r.readLong();
+ if (fileMagic == magic && magic != 0 && magic != id) {
+ return magic;
+ }
+ }
+ }
+ } catch (IOException ex) {
+ Log.v(TAG, "got exception checking file magic: " + ex);
+ }
+ }
+ }
+ if (VERBOSE) {
+ Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + "; magic: " + magic + "; id: " + id + "; ");
+ }
+ }
+
+ // If we can't retrieve the thumbnail, first check if there is one embedded in the
+ // EXIF data. If not, or it's not big enough, decompress the full size image.
+ Bitmap bitmap = null;
+ String filePath = null;
+ synchronized (c) {
+ if (c.moveToPosition(i)) {
+ filePath = c.getString(indexData());
+ }
+ }
+ if (filePath != null) {
+ String mimeType = c.getString(indexMimeType());
+ boolean isVideo = isVideoMimeType(mimeType);
+ if (isVideo) {
+ bitmap = createVideoThumbnail(filePath);
+ } else {
+ bitmap = createThumbnailFromEXIF(filePath, id);
+ if (bitmap == null) {
+ bitmap = createThumbnailFromUri(c, id);
+ }
+ }
+ synchronized (c) {
+ int degrees = 0;
+ if (c.moveToPosition(i)) {
+ int column = indexOrientation();
+ if (column >= 0)
+ degrees = c.getInt(column);
+ }
+ if (degrees != 0) {
+ Bitmap b2 = rotate(bitmap, degrees);
+ if (b2 != bitmap)
+ bitmap.recycle();
+ bitmap = b2;
+ }
+ }
+ }
+
+ // make a new magic number since things are out of sync
+ do {
+ magic = mRandom.nextLong();
+ } while (magic == 0);
+ if (bitmap != null) {
+ byte [] data = miniThumbData(bitmap);
+ if (createdThumbnailData != null) {
+ createdThumbnailData[0] = data;
+ }
+ saveMiniThumbToFile(data, id, magic);
+ }
+
+ synchronized (c) {
+ c.moveToPosition(i);
+ c.updateLong(indexMiniThumbId(), magic);
+ c.commitUpdates();
+ c.requery();
+ c.moveToPosition(i);
+
+ if (existingImage != null) {
+ existingImage.mMiniThumbMagic = magic;
+ }
+ return magic;
+ }
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) {
+ Cursor c = Images.Media.query(
+ mContentResolver,
+ mBaseUri,
+ new String[] { "_id", "mini_thumb_magic" },
+ thumbnailWhereClause(),
+ thumbnailWhereClauseArgs(),
+ "_id ASC");
+
+ int count = c.getCount();
+ if (VERBOSE)
+ Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows");
+
+ c.close();
+
+ if (!ImageManager.hasStorage()) {
+ if (VERBOSE)
+ Log.v(TAG, "bailing from the image checker thread -- no storage");
+ return;
+ }
+
+ String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
+ File oldFile = new File(oldPath);
+
+ if (count == 0) {
+ // now check that we have the right thumbs file
+// Log.v(TAG, "count is zero but oldFile.exists() is " + oldFile.exists());
+ if (!oldFile.exists()) {
+ return;
+ }
+ }
+
+ c = getCursor();
+ try {
+ if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount());
+ int current = 0;
+ for (int i = 0; i < c.getCount(); i++) {
+ try {
+ checkThumbnail(null, c, i);
+ } catch (Exception ex) {
+ Log.e(TAG, "!!!!! failed to check thumbnail... was the sd card removed?");
+ break;
+ }
+ if (cb != null) {
+ if (!cb.checking(current, totalThumbnails)) {
+ if (VERBOSE) Log.v(TAG, "got false from checking... break <<<<<<<<<<<<<<<<<<<<<<<<");
+ break;
+ }
+ }
+ current += 1;
+ }
+ } finally {
+ if (VERBOSE) Log.v(TAG, "checkThumbnails existing after reaching count " + c.getCount());
+ try {
+ oldFile.delete();
+ } catch (Exception ex) {
+ // ignore
+ }
+ }
+ }
+
+ protected String thumbnailWhereClause() {
+ return sMiniThumbIsNull + " and " + sWhereClause;
+ }
+
+ protected String[] thumbnailWhereClauseArgs() {
+ return sAcceptableImageTypes;
+ }
+
+ public void commitChanges() {
+ synchronized (mCursor) {
+ mCursor.commitUpdates();
+ requery();
+ }
+ }
+ protected Uri contentUri(long id) {
+ try {
+ // does our uri already have an id (single image query)?
+ // if so just return it
+ long existingId = ContentUris.parseId(mBaseUri);
+ if (existingId != id)
+ Log.e(TAG, "id mismatch");
+ return mBaseUri;
+ } catch (NumberFormatException ex) {
+ // otherwise tack on the id
+ return ContentUris.withAppendedId(mBaseUri, id);
+ }
+ }
+
+ public void deactivate() {
+ mCursorDeactivated = true;
+ try {
+ mCursor.deactivate();
+ } catch (IllegalStateException e) {
+ // IllegalStateException may be thrown if the cursor is stale.
+ Log.e(TAG, "Caught exception while deactivating cursor.", e);
+ }
+ if (mMiniThumbData != null) {
+ try {
+ mMiniThumbData.close();
+ mMiniThumbData = null;
+ } catch (IOException ex) {
+
+ }
+ }
+ }
+
+ public void dump(String msg) {
+ int count = getCount();
+ if (VERBOSE) Log.v(TAG, "dump ImageList (count is " + count + ") " + msg);
+ for (int i = 0; i < count; i++) {
+ IImage img = getImageAt(i);
+ if (img == null)
+ if (VERBOSE) Log.v(TAG, " " + i + ": " + "null");
+ else
+ if (VERBOSE) Log.v(TAG, " " + i + ": " + img.toString());
+ }
+ if (VERBOSE) Log.v(TAG, "end of dump container");
+ }
+ public int getCount() {
+ Cursor c = getCursor();
+ synchronized (c) {
+ try {
+ return c.getCount();
+ } catch (Exception ex) {
+ }
+ return 0;
+ }
+ }
+
+ public boolean isEmpty() {
+ return getCount() == 0;
+ }
+
+ protected Cursor getCursor() {
+ synchronized (mCursor) {
+ if (mCursorDeactivated) {
+ activateCursor();
+ }
+ return mCursor;
+ }
+ }
+
+ protected void activateCursor() {
+ requery();
+ }
+
+ public IImage getImageAt(int i) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ boolean moved;
+ try {
+ moved = c.moveToPosition(i);
+ } catch (Exception ex) {
+ return null;
+ }
+ if (moved) {
+ try {
+ long id = c.getLong(0);
+ long miniThumbId = 0;
+ int rotation = 0;
+ if (indexMiniThumbId() != -1) {
+ miniThumbId = c.getLong(indexMiniThumbId());
+ }
+ if (indexOrientation() != -1) {
+ rotation = c.getInt(indexOrientation());
+ }
+ long timestamp = c.getLong(1);
+ IImage img = mCache.get(id);
+ if (img == null) {
+ img = make(id, miniThumbId, mContentResolver, this, timestamp, i, rotation);
+ mCache.put(id, img);
+ }
+ return img;
+ } catch (Exception ex) {
+ Log.e(TAG, "got this exception trying to create image object: " + ex);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "unable to moveTo to " + i + "; count is " + c.getCount());
+ return null;
+ }
+ }
+ }
+ public IImage getImageForUri(Uri uri) {
+ // TODO make this a hash lookup
+ for (int i = 0; i < getCount(); i++) {
+ if (getImageAt(i).fullSizeImageUri().equals(uri)) {
+ return getImageAt(i);
+ }
+ }
+ return null;
+ }
+ private byte [] getMiniThumbFromFile(long id, byte [] data, long magicCheck) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return null;
+
+ long pos = id * sBytesPerMiniThumb;
+ RandomAccessFile f = r;
+ synchronized (f) {
+ try {
+ f.seek(pos);
+ if (f.readByte() == 1) {
+ long magic = f.readLong();
+ if (magic != magicCheck) {
+ if (VERBOSE) Log.v(TAG, "for id " + id + "; magic: " + magic + "; magicCheck: " + magicCheck + " (fail)");
+ return null;
+ }
+ int length = f.readInt();
+ f.read(data, 0, length);
+ return data;
+ } else {
+ return null;
+ }
+ } catch (IOException ex) {
+ long fileLength;
+ try {
+ fileLength = f.length();
+ } catch (IOException ex1) {
+ fileLength = -1;
+ }
+ if (VERBOSE) {
+ Log.e(TAG, "couldn't read thumbnail for " + id + "; " + ex.toString() + "; pos is " + pos + "; length is " + fileLength);
+ }
+ return null;
+ }
+ }
+ }
+ protected int getRowFor(IImage imageObj) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ int index = 0;
+ long targetId = imageObj.fullSizeImageId();
+ if (c.moveToFirst()) {
+ do {
+ if (c.getLong(0) == targetId) {
+ return index;
+ }
+ index += 1;
+ } while (c.moveToNext());
+ }
+ return -1;
+ }
+ }
+
+ protected abstract int indexOrientation();
+ protected abstract int indexDateTaken();
+ protected abstract int indexDescription();
+ protected abstract int indexMimeType();
+ protected abstract int indexData();
+ protected abstract int indexId();
+ protected abstract int indexLatitude();
+ protected abstract int indexLongitude();
+ protected abstract int indexMiniThumbId();
+ protected abstract int indexPicasaWeb();
+ protected abstract int indexPrivate();
+ protected abstract int indexTitle();
+ protected abstract int indexDisplayName();
+ protected abstract int indexThumbId();
+
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
+ return null;
+ }
+
+ protected abstract Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options);
+
+ public boolean removeImage(IImage image) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ /*
+ * TODO: consider putting the image in a holding area so
+ * we can get it back as needed
+ * TODO: need to delete the thumbnails as well
+ */
+ boolean moved;
+ try {
+ moved = c.moveToPosition(image.getRow());
+ } catch (Exception ex) {
+ Log.e(TAG, "removeImage got exception " + ex.toString());
+ return false;
+ }
+ if (moved) {
+ Uri u = image.fullSizeImageUri();
+ mContentResolver.delete(u, null, null);
+ image.onRemove();
+ requery();
+ }
+ }
+ return true;
+ }
+
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImageList#removeImageAt(int)
+ */
+ public void removeImageAt(int i) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ /*
+ * TODO: consider putting the image in a holding area so
+ * we can get it back as needed
+ * TODO: need to delete the thumbnails as well
+ */
+ dump("before delete");
+ IImage image = getImageAt(i);
+ boolean moved;
+ try {
+ moved = c.moveToPosition(i);
+ } catch (Exception ex) {
+ return;
+ }
+ if (moved) {
+ Uri u = image.fullSizeImageUri();
+ mContentResolver.delete(u, null, null);
+ requery();
+ image.onRemove();
+ }
+ dump("after delete");
+ }
+ }
+
+ public void removeOnChangeListener(OnChange changeCallback) {
+ if (changeCallback == mListener)
+ mListener = null;
+ }
+
+ protected void requery() {
+ mCache.clear();
+ mCursor.requery();
+ mCursorDeactivated = false;
+ }
+
+ protected void saveMiniThumbToFile(Bitmap bitmap, long id, long magic) throws IOException {
+ byte[] data = miniThumbData(bitmap);
+ saveMiniThumbToFile(data, id, magic);
+ }
+
+ protected void saveMiniThumbToFile(byte[] data, long id, long magic) throws IOException {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return;
+
+ long pos = id * sBytesPerMiniThumb;
+ long t0 = System.currentTimeMillis();
+ synchronized (r) {
+ try {
+ long t1 = System.currentTimeMillis();
+ long t2 = System.currentTimeMillis();
+ if (data != null) {
+ if (data.length > sBytesPerMiniThumb) {
+ if (VERBOSE) Log.v(TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!! " + data.length + " > " + sBytesPerMiniThumb);
+ return;
+ }
+ r.seek(pos);
+ r.writeByte(0); // we have no data in this slot
+
+ // if magic is 0 then leave it alone
+ if (magic == 0)
+ r.skipBytes(8);
+ else
+ r.writeLong(magic);
+ r.writeInt(data.length);
+ r.write(data);
+ // f.flush();
+ r.seek(pos);
+ r.writeByte(1); // we have data in this slot
+ long t3 = System.currentTimeMillis();
+
+ if (VERBOSE) Log.v(TAG, "saveMiniThumbToFile took " + (t3-t0) + "; " + (t1-t0) + " " + (t2-t1) + " " + (t3-t2));
+ }
+ } catch (IOException ex) {
+ Log.e(TAG, "couldn't save mini thumbnail data for " + id + "; " + ex.toString());
+ throw ex;
+ }
+ }
+ }
+
+ public void setOnChangeListener(OnChange changeCallback, Handler h) {
+ mListener = changeCallback;
+ mHandler = h;
+ }
+ }
+
+ public class CanceledException extends Exception {
+
+ }
+ public enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL }
+
+ public interface IAddImage_cancelable extends ICancelable {
+ public void get();
+ }
+
+ /*
+ * The model for canceling an in-progress image save is this. For any
+ * given part of the task of saving return an ICancelable. The "result"
+ * from an ICancelable can be retrieved using the get* method. If the
+ * operation was canceled then null is returned. The act of canceling
+ * is to call "cancel" -- from another thread.
+ *
+ * In general an object which implements ICancelable will need to
+ * check, periodically, whether they are canceled or not. This works
+ * well for some things and less well for others.
+ *
+ * Right now the actual jpeg encode does not check cancelation but
+ * the part of encoding which writes the data to disk does. Note,
+ * though, that there is what appears to be a bug in the jpeg encoder
+ * in that if the stream that's being written is closed it crashes
+ * rather than returning an error. TODO fix that.
+ *
+ * When an object detects that it is canceling it must, before exiting,
+ * call acknowledgeCancel. This is necessary because the caller of
+ * cancel() will block until acknowledgeCancel is called.
+ */
+ public interface ICancelable {
+ /*
+ * call cancel() when the unit of work in progress needs to be
+ * canceled. This should return true if it was possible to
+ * cancel and false otherwise. If this returns false the caller
+ * may still be able to cleanup and simulate cancelation.
+ */
+ public boolean cancel();
+ }
+
+ public interface IGetBitmap_cancelable extends ICancelable {
+ // returns the bitmap or null if there was an error or we were canceled
+ public Bitmap get();
+ };
+ public interface IGetBoolean_cancelable extends ICancelable {
+ public boolean get();
+ }
+ public interface IImage {
+
+ public abstract void commitChanges();
+
+ /**
+ * Get the bitmap for the full size image.
+ * @return the bitmap for the full size image.
+ */
+ public abstract Bitmap fullSizeBitmap(int targetWidthOrHeight);
+
+ /**
+ *
+ * @return an object which can be canceled while the bitmap is loading
+ */
+ public abstract IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthOrHeight);
+
+ /**
+ * Gets the input stream associated with a given full size image.
+ * This is used, for example, if one wants to email or upload
+ * the image.
+ * @return the InputStream associated with the image.
+ */
+ public abstract InputStream fullSizeImageData();
+ public abstract long fullSizeImageId();
+ public abstract Uri fullSizeImageUri();
+ public abstract IImageList getContainer();
+ public abstract long getDateTaken();
+
+ /**
+ * Gets the description of the image.
+ * @return the description of the image.
+ */
+ public abstract String getDescription();
+ public abstract String getMimeType();
+ public abstract int getHeight();
+
+ /**
+ * Gets the flag telling whether this video/photo is private or public.
+ * @return the description of the image.
+ */
+ public abstract boolean getIsPrivate();
+
+ public abstract double getLatitude();
+
+ public abstract double getLongitude();
+
+ /**
+ * Gets the name of the image.
+ * @return the name of the image.
+ */
+ public abstract String getTitle();
+
+ public abstract String getDisplayName();
+
+ public abstract String getPicasaId();
+
+ public abstract int getRow();
+
+ public abstract int getWidth();
+
+ public abstract boolean hasLatLong();
+
+ public abstract long imageId();
+
+ public abstract boolean isReadonly();
+
+ public abstract boolean isDrm();
+
+ public abstract Bitmap miniThumbBitmap();
+
+ public abstract void onRemove();
+
+ public abstract boolean rotateImageBy(int degrees);
+
+ /**
+ * Sets the description of the image.
+ */
+ public abstract void setDescription(String description);
+
+ /**
+ * Sets whether the video/photo is private or public.
+ */
+ public abstract void setIsPrivate(boolean isPrivate);
+
+ /**
+ * Sets the name of the image.
+ */
+ public abstract void setName(String name);
+
+ public abstract void setPicasaId(String id);
+
+ /**
+ * Get the bitmap for the medium thumbnail.
+ * @return the bitmap for the medium thumbnail.
+ */
+ public abstract Bitmap thumbBitmap();
+
+ public abstract Uri thumbUri();
+
+ public abstract String getDataPath();
+ }
+
+ public interface IImageList {
+ public HashMap<String, String> getBucketIds();
+
+ public interface OnChange {
+ public void onChange(IImageList list);
+ }
+
+ public interface ThumbCheckCallback {
+ public boolean checking(int current, int count);
+ }
+
+ public abstract void checkThumbnails(ThumbCheckCallback cb, int totalCount);
+
+ public abstract void commitChanges();
+
+ public abstract void deactivate();
+
+ /**
+ * Returns the count of image objects.
+ *
+ * @return the number of images
+ */
+ public abstract int getCount();
+
+ /**
+ * @return true if the count of image objects is zero.
+ */
+
+ public abstract boolean isEmpty();
+
+ /**
+ * Returns the image at the ith position.
+ *
+ * @param i the position
+ * @return the image at the ith position
+ */
+ public abstract IImage getImageAt(int i);
+
+ /**
+ * Returns the image with a particular Uri.
+ *
+ * @param uri
+ * @return the image with a particular Uri.
+ */
+ public abstract IImage getImageForUri(Uri uri);;
+
+ /**
+ *
+ * @param image
+ * @return true if the image was removed.
+ */
+ public abstract boolean removeImage(IImage image);
+ /**
+ * Removes the image at the ith position.
+ * @param i the position
+ */
+ public abstract void removeImageAt(int i);
+
+ public abstract void removeOnChangeListener(OnChange changeCallback);
+ public abstract void setOnChangeListener(OnChange changeCallback, Handler h);
+ }
+
+ class Image extends BaseImage implements IImage {
+ int mRotation;
+
+ protected Image(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow, int rotation) {
+ super(id, miniThumbId, cr, container, cursorRow);
+ mRotation = rotation;
+ }
+
+ public String getDataPath() {
+ String path = null;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ int column = ((ImageList)getContainer()).indexData();
+ if (column >= 0)
+ path = c.getString(column);
+ }
+ }
+ return path;
+ }
+
+ protected int getDegreesRotated() {
+ return mRotation;
+ }
+
+ protected void setDegreesRotated(int degrees) {
+ Cursor c = getCursor();
+ mRotation = degrees;
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ int column = ((ImageList)getContainer()).indexOrientation();
+ if (column >= 0) {
+ c.updateInt(column, degrees);
+ getContainer().commitChanges();
+ }
+ }
+ }
+ }
+
+ protected Bitmap.CompressFormat compressionType() {
+ String mimeType = getMimeType();
+ if (mimeType == null)
+ return Bitmap.CompressFormat.JPEG;
+
+ if (mimeType.equals("image/png"))
+ return Bitmap.CompressFormat.PNG;
+ else if (mimeType.equals("image/gif"))
+ return Bitmap.CompressFormat.PNG;
+
+ return Bitmap.CompressFormat.JPEG;
+ }
+
+ /**
+ * Does not replace the tag if already there. Otherwise, adds to the exif tags.
+ * @param tag
+ * @param value
+ */
+ public void addExifTag(String tag, String value) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ if (!mExifData.containsKey(tag)) {
+ mExifData.put(tag, value);
+ } else {
+ if (VERBOSE) Log.v(TAG, "addExifTag where the key already was there: " + tag + " = " + value);
+ }
+ }
+
+ /**
+ * Return the value of the Exif tag as an int. Returns 0 on any type of error.
+ * @param tag
+ * @return
+ */
+ public int getExifTagInt(String tag) {
+ if (mExifData != null) {
+ String tagValue = mExifData.get(tag);
+ if (tagValue != null) {
+ return Integer.parseInt(tagValue);
+ }
+ }
+ return 0;
+ }
+
+ public boolean isReadonly() {
+ String mimeType = getMimeType();
+ return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType);
+ }
+
+ public boolean isDrm() {
+ return false;
+ }
+
+ /**
+ * Remove tag if already there. Otherwise, does nothing.
+ * @param tag
+ */
+ public void removeExifTag(String tag) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ mExifData.remove(tag);
+ }
+
+ /**
+ * Replaces the tag if already there. Otherwise, adds to the exif tags.
+ * @param tag
+ * @param value
+ */
+ public void replaceExifTag(String tag, String value) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ if (!mExifData.containsKey(tag)) {
+ mExifData.remove(tag);
+ }
+ mExifData.put(tag, value);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#saveModifiedImage(android.graphics.Bitmap)
+ */
+ public IGetBoolean_cancelable saveImageContents(
+ final Bitmap image,
+ final byte [] jpegData,
+ final int orientation,
+ final boolean newFile,
+ final Cursor cursor) {
+ final class SaveImageContentsCancelable extends BaseCancelable implements IGetBoolean_cancelable {
+ IGetBoolean_cancelable mCurrentCancelable = null;
+
+ SaveImageContentsCancelable() {
+ }
+
+ public boolean doCancelWork() {
+ synchronized (this) {
+ if (mCurrentCancelable != null)
+ mCurrentCancelable.cancel();
+ }
+ return true;
+ }
+
+ public boolean get() {
+ try {
+ Bitmap thumbnail = null;
+
+ long t1 = System.currentTimeMillis();
+ Uri uri = mContainer.contentUri(mId);
+ synchronized (this) {
+ checkCanceled();
+ mCurrentCancelable = compressImageToFile(image, jpegData, uri);
+ }
+
+ long t2 = System.currentTimeMillis();
+ if (!mCurrentCancelable.get())
+ return false;
+
+ synchronized (this) {
+ String filePath;
+ synchronized (cursor) {
+ cursor.moveToPosition(0);
+ filePath = cursor.getString(2);
+ }
+ // TODO: If thumbData is present and usable, we should call the version
+ // of storeThumbnail which takes a byte array, rather than re-encoding
+ // a new JPEG of the same dimensions.
+ byte [] thumbData = null;
+ synchronized (ImageManager.instance()) {
+ thumbData = (new ExifInterface(filePath)).getThumbnail();
+ }
+ if (VERBOSE) Log.v(TAG, "for file " + filePath + " thumbData is " + thumbData + "; length " + (thumbData!=null ? thumbData.length : -1));
+ if (thumbData != null) {
+ thumbnail = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length);
+ if (VERBOSE) Log.v(TAG, "embedded thumbnail bitmap " + thumbnail.getWidth() + "/" + thumbnail.getHeight());
+ }
+ if (thumbnail == null && image != null) {
+ thumbnail = image;
+ }
+ if (thumbnail == null && jpegData != null) {
+ thumbnail = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
+ }
+ }
+
+ long t3 = System.currentTimeMillis();
+ mContainer.storeThumbnail(thumbnail, Image.this.fullSizeImageId());
+ long t4 = System.currentTimeMillis();
+ checkCanceled();
+ if (VERBOSE) Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>> rotating by " + orientation);
+ try {
+ saveMiniThumb(rotate(thumbnail, orientation));
+ } catch (IOException e) {
+ // Ignore if unable to save thumb.
+ }
+ long t5 = System.currentTimeMillis();
+ checkCanceled();
+
+ if (VERBOSE) Log.v(TAG, String.format("Timing data %d %d %d %d", t2-t1, t3-t2, t4-t3, t5-t4));
+ return true;
+ } catch (CanceledException ex) {
+ if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup");
+ return false;
+ } finally {
+ /*
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveTo(getRow())) {
+ mContainer.requery();
+ }
+ }
+ */
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new SaveImageContentsCancelable();
+ }
+
+ private void setExifRotation(int degrees) {
+ try {
+ Cursor c = getCursor();
+ String filePath;
+ synchronized (c) {
+ filePath = c.getString(mContainer.indexData());
+ }
+ synchronized (ImageManager.instance()) {
+ ExifInterface exif = new ExifInterface(filePath);
+ if (mExifData == null) {
+ mExifData = exif.getAttributes();
+ }
+ if (degrees < 0)
+ degrees += 360;
+
+ int orientation = ExifInterface.ORIENTATION_NORMAL;
+ switch (degrees) {
+ case 0:
+ orientation = ExifInterface.ORIENTATION_NORMAL;
+ break;
+ case 90:
+ orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ case 180:
+ orientation = ExifInterface.ORIENTATION_ROTATE_180;
+ break;
+ case 270:
+ orientation = ExifInterface.ORIENTATION_ROTATE_270;
+ break;
+ }
+
+ replaceExifTag(ExifInterface.TAG_ORIENTATION, Integer.toString(orientation));
+ replaceExifTag("UserComment", "saveRotatedImage comment orientation: " + orientation);
+ exif.saveAttributes(mExifData);
+ exif.commitChanges();
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "unable to save exif data with new orientation " + fullSizeImageUri());
+ }
+ }
+
+ /**
+ * Save the rotated image by updating the Exif "Orientation" tag.
+ * @param degrees
+ * @return
+ */
+ public boolean rotateImageBy(int degrees) {
+ int newDegrees = getDegreesRotated() + degrees;
+ setExifRotation(newDegrees);
+ setDegreesRotated(newDegrees);
+
+ // setting this to zero will force the call to checkCursor to generate fresh thumbs
+ mMiniThumbMagic = 0;
+ try {
+ mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow());
+ } catch (IOException e) {
+ // Ignore inability to store mini thumbnail.
+ }
+
+ return true;
+ }
+
+ public Bitmap thumbBitmap() {
+ Bitmap bitmap = null;
+ Cursor c = null;
+ if (mContainer.mThumbUri != null) {
+ try {
+ c = mContentResolver.query(
+ mContainer.mThumbUri,
+ THUMB_PROJECTION,
+ Thumbnails.IMAGE_ID + "=?",
+ new String[] { String.valueOf(fullSizeImageId()) },
+ null);
+ if (c != null && c.moveToFirst()) {
+ Uri thumbUri = ContentUris.withAppendedId(mContainer.mThumbUri, c.getLong(((ImageList)mContainer).INDEX_THUMB_ID));
+ ParcelFileDescriptor pfdInput;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r");
+ bitmap = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ pfdInput.close();
+ } catch (FileNotFoundException ex) {
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ } catch (NullPointerException ex) {
+ // we seem to get this if the file doesn't exist anymore
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ }
+ }
+ } catch (Exception ex) {
+ // sdcard removed?
+ return null;
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ }
+
+ if (bitmap == null) {
+ bitmap = fullSizeBitmap(THUMBNAIL_TARGET_SIZE, false);
+ if (VERBOSE) {
+ Log.v(TAG, "no thumbnail found... storing new one for " + fullSizeImageId());
+ }
+ bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId());
+ }
+
+ if (bitmap != null) {
+ int degrees = getDegreesRotated();
+ if (degrees != 0) {
+ Matrix m = new Matrix();
+ m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
+ m, true);
+ }
+ }
+
+ long elapsed = System.currentTimeMillis();
+ return bitmap;
+ }
+
+ }
+
+ final static private String sWhereClause = "(" + Images.Media.MIME_TYPE + " in (?, ?, ?))";
+ final static private String[] sAcceptableImageTypes = new String[] { "image/jpeg", "image/png", "image/gif" };
+ final static private String sMiniThumbIsNull = "mini_thumb_magic isnull";
+
+ private static final String[] IMAGE_PROJECTION = new String[] {
+ "_id",
+ "_data",
+ ImageColumns.DATE_TAKEN,
+ ImageColumns.MINI_THUMB_MAGIC,
+ ImageColumns.ORIENTATION,
+ ImageColumns.MIME_TYPE
+ };
+
+ /**
+ * Represents an ordered collection of Image objects.
+ * Provides an api to add and remove an image.
+ */
+ class ImageList extends BaseImageList implements IImageList {
+ final int INDEX_ID = indexOf(IMAGE_PROJECTION, "_id");
+ final int INDEX_DATA = indexOf(IMAGE_PROJECTION, "_data");
+ final int INDEX_MIME_TYPE = indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE);
+ final int INDEX_DATE_TAKEN = indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN);
+ final int INDEX_MINI_THUMB_MAGIC = indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC);
+ final int INDEX_ORIENTATION = indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION);
+
+ final int INDEX_THUMB_ID = indexOf(THUMB_PROJECTION, BaseColumns._ID);
+ final int INDEX_THUMB_IMAGE_ID = indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID);
+ final int INDEX_THUMB_WIDTH = indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH);
+ final int INDEX_THUMB_HEIGHT = indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT);
+
+ boolean mIsRegistered = false;
+ ContentObserver mContentObserver;
+ DataSetObserver mDataSetObserver;
+
+ public HashMap<String, String> getBucketIds() {
+ Cursor c = Images.Media.query(
+ mContentResolver,
+ mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(),
+ new String[] {
+ ImageColumns.BUCKET_DISPLAY_NAME,
+ ImageColumns.BUCKET_ID
+ },
+ whereClause(),
+ whereClauseArgs(),
+ sortOrder());
+
+ HashMap<String, String> hash = new HashMap<String, String>();
+ if (c != null && c.moveToFirst()) {
+ do {
+ hash.put(c.getString(1), c.getString(0));
+ } while (c.moveToNext());
+ }
+ return hash;
+ }
+ /**
+ * ImageList constructor.
+ * @param cr ContentResolver
+ */
+ public ImageList(Context ctx, ContentResolver cr, Uri imageUri, Uri thumbUri, int sort, String bucketId) {
+ super(ctx, cr, imageUri, sort, bucketId);
+ mBaseUri = imageUri;
+ mThumbUri = thumbUri;
+ mSort = sort;
+
+ mContentResolver = cr;
+
+ mCursor = createCursor();
+ if (mCursor == null) {
+ Log.e(TAG, "unable to create image cursor for " + mBaseUri);
+ throw new UnsupportedOperationException();
+ }
+
+ if (VERBOSE) {
+ Log.v(TAG, "for " + mBaseUri.toString() + " got cursor " + mCursor + " with length " + (mCursor != null ? mCursor.getCount() : "-1"));
+ }
+
+ final Runnable updateRunnable = new Runnable() {
+ public void run() {
+ // handling these external updates is causing ANR problems that are unresolved.
+ // For now ignore them since there shouldn't be anyone modifying the database on the fly.
+ if (true)
+ return;
+
+ synchronized (mCursor) {
+ requery();
+ }
+ if (mListener != null)
+ mListener.onChange(ImageList.this);
+ }
+ };
+
+ mContentObserver = new ContentObserver(null) {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return false;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (VERBOSE) Log.v(TAG, "MyContentObserver.onChange; selfChange == " + selfChange);
+ updateRunnable.run();
+ }
+ };
+
+ mDataSetObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onChanged");
+// updateRunnable.run();
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onInvalidated: " + mCursorDeactivated);
+ }
+ };
+
+ registerObservers();
+ }
+
+ private void registerObservers() {
+ if (mIsRegistered)
+ return;
+
+ mCursor.registerContentObserver(mContentObserver);
+ mCursor.registerDataSetObserver(mDataSetObserver);
+ mIsRegistered = true;
+ }
+
+ private void unregisterObservers() {
+ if (!mIsRegistered)
+ return;
+
+ mCursor.unregisterContentObserver(mContentObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mIsRegistered = false;
+ }
+
+ public void deactivate() {
+ super.deactivate();
+ unregisterObservers();
+ }
+
+ protected void activateCursor() {
+ super.activateCursor();
+ registerObservers();
+ }
+
+ protected String whereClause() {
+ if (mBucketId != null) {
+ return sWhereClause + " and " + Images.Media.BUCKET_ID + " = '" + mBucketId + "'";
+ } else {
+ return sWhereClause;
+ }
+ }
+
+ protected String[] whereClauseArgs() {
+ return sAcceptableImageTypes;
+ }
+
+ protected Cursor createCursor() {
+ Cursor c =
+ Images.Media.query(
+ mContentResolver,
+ mBaseUri,
+ IMAGE_PROJECTION,
+ whereClause(),
+ whereClauseArgs(),
+ sortOrder());
+ if (VERBOSE)
+ Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount()));
+ return c;
+ }
+
+ protected int indexOrientation() { return INDEX_ORIENTATION; }
+ protected int indexDateTaken() { return INDEX_DATE_TAKEN; }
+ protected int indexDescription() { return -1; }
+ protected int indexMimeType() { return INDEX_MIME_TYPE; }
+ protected int indexData() { return INDEX_DATA; }
+ protected int indexId() { return INDEX_ID; }
+ protected int indexLatitude() { return -1; }
+ protected int indexLongitude() { return -1; }
+ protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; }
+
+ protected int indexPicasaWeb() { return -1; }
+ protected int indexPrivate() { return -1; }
+ protected int indexTitle() { return -1; }
+ protected int indexDisplayName() { return -1; }
+ protected int indexThumbId() { return INDEX_THUMB_ID; }
+
+ @Override
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
+ return new Image(id, miniThumbId, mContentResolver, this, index, rotation);
+ }
+
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfd, BitmapFactory.Options options) {
+ Bitmap b = null;
+
+ try {
+ if (pfd == null)
+ pfd = makeInputStream(uri);
+
+ if (pfd == null)
+ return null;
+
+ if (options == null)
+ options = new BitmapFactory.Options();
+
+ java.io.FileDescriptor fd = pfd.getFileDescriptor();
+ options.inSampleSize = 1;
+ if (targetWidthHeight != -1) {
+ options.inJustDecodeBounds = true;
+ long t1 = System.currentTimeMillis();
+ BitmapFactory.decodeFileDescriptor(fd, null, options);
+ long t2 = System.currentTimeMillis();
+ if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) {
+ return null;
+ }
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+ options.inJustDecodeBounds = false;
+ }
+
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ long t1 = System.currentTimeMillis();
+ b = BitmapFactory.decodeFileDescriptor(fd, null, options);
+ long t2 = System.currentTimeMillis();
+ if (VERBOSE) {
+ Log.v(TAG, "A: got bitmap " + b + " with sampleSize " + options.inSampleSize + " took " + (t2-t1));
+ }
+ } catch (OutOfMemoryError ex) {
+ if (VERBOSE) Log.v(TAG, "got oom exception " + ex);
+ return null;
+ } finally {
+ try {
+ pfd.close();
+ } catch (IOException ex) {
+ }
+ }
+ return b;
+ }
+
+ private ParcelFileDescriptor makeInputStream(Uri uri) {
+ try {
+ return mContentResolver.openFileDescriptor(uri, "r");
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ private String sortOrder() {
+ // add id to the end so that we don't ever get random sorting
+ // which could happen, I suppose, if the first two values were
+ // duplicated
+ String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
+ return
+ Images.Media.DATE_TAKEN + ascending + "," +
+ Images.Media._ID + ascending;
+ }
+
+ }
+
+ /**
+ * Represents an ordered collection of Image objects from the DRM provider.
+ */
+ class DrmImageList extends ImageList implements IImageList {
+ private final String[] DRM_IMAGE_PROJECTION = new String[] {
+ DrmStore.Audio._ID,
+ DrmStore.Audio.DATA,
+ DrmStore.Audio.MIME_TYPE,
+ };
+
+ final int INDEX_ID = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio._ID);
+ final int INDEX_MIME_TYPE = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio.MIME_TYPE);
+
+ public DrmImageList(Context ctx, ContentResolver cr, Uri imageUri, int sort, String bucketId) {
+ super(ctx, cr, imageUri, null, sort, bucketId);
+ }
+
+ protected Cursor createCursor() {
+ return mContentResolver.query(mBaseUri, DRM_IMAGE_PROJECTION, null, null, sortOrder());
+ }
+
+ @Override
+ public void checkThumbnails(ThumbCheckCallback cb, int totalCount) {
+ // do nothing
+ }
+
+ @Override
+ public long checkThumbnail(BaseImage existingImage, Cursor c, int i) {
+ return 0;
+ }
+
+ class DrmImage extends Image {
+ protected DrmImage(long id, ContentResolver cr, BaseImageList container, int cursorRow) {
+ super(id, 0, cr, container, cursorRow, 0);
+ }
+
+ public boolean isDrm() {
+ return true;
+ }
+
+ public boolean isReadonly() {
+ return true;
+ }
+
+ public Bitmap miniThumbBitmap() {
+ return fullSizeBitmap(MINI_THUMB_TARGET_SIZE);
+ }
+
+ public Bitmap thumbBitmap() {
+ return fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
+ }
+
+ public String getDisplayName() {
+ return getTitle();
+ }
+ }
+
+ @Override
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
+ return new DrmImage(id, mContentResolver, this, index);
+ }
+
+ protected int indexOrientation() { return -1; }
+ protected int indexDateTaken() { return -1; }
+ protected int indexDescription() { return -1; }
+ protected int indexMimeType() { return -1; }
+ protected int indexId() { return -1; }
+ protected int indexLatitude() { return -1; }
+ protected int indexLongitude() { return -1; }
+ protected int indexMiniThumbId() { return -1; }
+ protected int indexPicasaWeb() { return -1; }
+ protected int indexPrivate() { return -1; }
+ protected int indexTitle() { return -1; }
+ protected int indexDisplayName() { return -1; }
+ protected int indexThumbId() { return -1; }
+
+ // TODO review this probably should be based on DATE_TAKEN same as images
+ private String sortOrder() {
+ String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
+ return
+ DrmStore.Images.TITLE + ascending + "," +
+ DrmStore.Images._ID;
+ }
+ }
+
+ class ImageListUber implements IImageList {
+ private IImageList [] mSubList;
+ private int mSort;
+ private IImageList.OnChange mListener = null;
+ Handler mHandler;
+
+ // This is an array of Longs wherein each Long consists of
+ // two components. The first component indicates the number of
+ // consecutive entries that belong to a given sublist.
+ // The second component indicates which sublist we're referring
+ // to (an int which is used to index into mSubList).
+ ArrayList<Long> mSkipList = null;
+
+ int [] mSkipCounts = null;
+
+ public HashMap<String, String> getBucketIds() {
+ HashMap<String, String> hashMap = new HashMap<String, String>();
+ for (IImageList list: mSubList) {
+ hashMap.putAll(list.getBucketIds());
+ }
+ return hashMap;
+ }
+
+ public ImageListUber(IImageList [] sublist, int sort) {
+ mSubList = sublist.clone();
+ mSort = sort;
+
+ if (mListener != null) {
+ for (IImageList list: sublist) {
+ list.setOnChangeListener(new OnChange() {
+ public void onChange(IImageList list) {
+ if (mListener != null) {
+ mListener.onChange(ImageListUber.this);
+ }
+ }
+ }, mHandler);
+ }
+ }
+ }
+
+ public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) {
+ for (IImageList i : mSubList) {
+ int count = i.getCount();
+ i.checkThumbnails(cb, totalThumbnails);
+ totalThumbnails -= count;
+ }
+ }
+
+ public void commitChanges() {
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ for (int i = 0; i < length; i++)
+ sublist[i].commitChanges();
+ }
+
+ public void deactivate() {
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ int pos = -1;
+ while (++pos < length) {
+ IImageList sub = sublist[pos];
+ sub.deactivate();
+ }
+ }
+
+ public int getCount() {
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ int count = 0;
+ for (int i = 0; i < length; i++)
+ count += sublist[i].getCount();
+ return count;
+ }
+
+ public boolean isEmpty() {
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ for (int i = 0; i < length; i++) {
+ if (! sublist[i].isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // mSkipCounts is used to tally the counts as we traverse
+ // the mSkipList. It's a member variable only so that
+ // we don't have to allocate each time through. Otherwise
+ // it could just as easily be a local.
+
+ public synchronized IImage getImageAt(int index) {
+ if (index < 0 || index > getCount())
+ throw new IndexOutOfBoundsException("index " + index + " out of range max is " + getCount());
+
+ // first make sure our allocations are in order
+ if (mSkipCounts == null || mSubList.length > mSkipCounts.length)
+ mSkipCounts = new int[mSubList.length];
+
+ if (mSkipList == null)
+ mSkipList = new ArrayList<Long>();
+
+ // zero out the mSkipCounts since that's only used for the
+ // duration of the function call
+ for (int i = 0; i < mSubList.length; i++)
+ mSkipCounts[i] = 0;
+
+ // a counter of how many images we've skipped in
+ // trying to get to index. alternatively we could
+ // have decremented index but, alas, I liked this
+ // way more.
+ int skipCount = 0;
+
+ // scan the existing mSkipList to see if we've computed
+ // enough to just return the answer
+ for (int i = 0; i < mSkipList.size(); i++) {
+ long v = mSkipList.get(i);
+
+ int offset = (int) (v & 0xFFFF);
+ int which = (int) (v >> 32);
+
+ if (skipCount + offset > index) {
+ int subindex = mSkipCounts[which] + (index - skipCount);
+ IImage img = mSubList[which].getImageAt(subindex);
+ return img;
+ }
+
+ skipCount += offset;
+ mSkipCounts[which] += offset;
+ }
+
+ // if we get here we haven't computed the answer for
+ // "index" yet so keep computing. This means running
+ // through the list of images and either modifying the
+ // last entry or creating a new one.
+ long count = 0;
+ while (true) {
+ long maxTimestamp = mSort == SORT_ASCENDING ? Long.MAX_VALUE : Long.MIN_VALUE;
+ int which = -1;
+ for (int i = 0; i < mSubList.length; i++) {
+ int pos = mSkipCounts[i];
+ IImageList list = mSubList[i];
+ if (pos < list.getCount()) {
+ IImage image = list.getImageAt(pos);
+ // this should never be null but sometimes the database is
+ // causing problems and it is null
+ if (image != null) {
+ long timestamp = image.getDateTaken();
+ if (mSort == SORT_ASCENDING ? (timestamp < maxTimestamp) : (timestamp > maxTimestamp)) {
+ maxTimestamp = timestamp;
+ which = i;
+ }
+ }
+ }
+ }
+
+ if (which == -1) {
+ if (VERBOSE) Log.v(TAG, "which is -1, returning null");
+ return null;
+ }
+
+ boolean done = false;
+ count = 1;
+ if (mSkipList.size() > 0) {
+ int pos = mSkipList.size() - 1;
+ long oldEntry = mSkipList.get(pos);
+ if ((oldEntry >> 32) == which) {
+ long newEntry = oldEntry + 1;
+ mSkipList.set(pos, newEntry);
+ done = true;
+ }
+ }
+ if (!done) {
+ long newEntry = ((long)which << 32) | count;
+ if (VERBOSE) {
+ Log.v(TAG, "new entry is " + Long.toHexString(newEntry));
+ }
+ mSkipList.add(newEntry);
+ }
+
+ if (skipCount++ == index) {
+ return mSubList[which].getImageAt(mSkipCounts[which]);
+ }
+ mSkipCounts[which] += 1;
+ }
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ // TODO perhaps we can preflight the base of the uri
+ // against each sublist first
+ for (int i = 0; i < mSubList.length; i++) {
+ IImage img = mSubList[i].getImageForUri(uri);
+ if (img != null)
+ return img;
+ }
+ return null;
+ }
+
+ /**
+ * Modify the skip list when an image is deleted by finding
+ * the relevant entry in mSkipList and decrementing the
+ * counter. This is simple because deletion can never
+ * cause change the order of images.
+ */
+ public void modifySkipCountForDeletedImage(int index) {
+ int skipCount = 0;
+
+ for (int i = 0; i < mSkipList.size(); i++) {
+ long v = mSkipList.get(i);
+
+ int offset = (int) (v & 0xFFFF);
+ int which = (int) (v >> 32);
+
+ if (skipCount + offset > index) {
+ mSkipList.set(i, v-1);
+ break;
+ }
+
+ skipCount += offset;
+ }
+ }
+
+ public boolean removeImage(IImage image) {
+ IImageList parent = image.getContainer();
+ int pos = -1;
+ int baseIndex = 0;
+ while (++pos < mSubList.length) {
+ IImageList sub = mSubList[pos];
+ if (sub == parent) {
+ if (sub.removeImage(image)) {
+ modifySkipCountForDeletedImage(baseIndex);
+ return true;
+ } else {
+ break;
+ }
+ }
+ baseIndex += sub.getCount();
+ }
+ return false;
+ }
+
+ public void removeImageAt(int index) {
+ IImage img = getImageAt(index);
+ if (img != null) {
+ IImageList list = img.getContainer();
+ if (list != null) {
+ list.removeImage(img);
+ modifySkipCountForDeletedImage(index);
+ }
+ }
+ }
+
+ public void removeOnChangeListener(OnChange changeCallback) {
+ if (changeCallback == mListener)
+ mListener = null;
+ }
+
+ public void setOnChangeListener(OnChange changeCallback, Handler h) {
+ mListener = changeCallback;
+ mHandler = h;
+ }
+
+ }
+
+ public static abstract class SimpleBaseImage implements IImage {
+ public void commitChanges() {
+ throw new UnsupportedOperationException();
+ }
+
+ public InputStream fullSizeImageData() {
+ throw new UnsupportedOperationException();
+ }
+
+ public long fullSizeImageId() {
+ return 0;
+ }
+
+ public Uri fullSizeImageUri() {
+ throw new UnsupportedOperationException();
+ }
+
+ public IImageList getContainer() {
+ return null;
+ }
+
+ public long getDateTaken() {
+ return 0;
+ }
+
+ public String getMimeType() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getDescription() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean getIsPrivate() {
+ throw new UnsupportedOperationException();
+ }
+
+ public double getLatitude() {
+ return 0D;
+ }
+
+ public double getLongitude() {
+ return 0D;
+ }
+
+ public String getTitle() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getDisplayName() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getPicasaId() {
+ return null;
+ }
+
+ public int getRow() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getHeight() {
+ return 0;
+ }
+
+ public int getWidth() {
+ return 0;
+ }
+
+ public boolean hasLatLong() {
+ return false;
+ }
+
+ public boolean isReadonly() {
+ return true;
+ }
+
+ public boolean isDrm() {
+ return false;
+ }
+
+ public void onRemove() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean rotateImageBy(int degrees) {
+ return false;
+ }
+
+ public void setDescription(String description) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setIsPrivate(boolean isPrivate) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setName(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setPicasaId(long id) {
+ }
+
+ public void setPicasaId(String id) {
+ }
+
+ public Uri thumbUri() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ class SingleImageList extends BaseImageList implements IImageList {
+ private IImage mSingleImage;
+ private ContentResolver mContentResolver;
+ private Uri mUri;
+
+ class UriImage extends SimpleBaseImage {
+
+ UriImage() {
+ }
+
+ public String getDataPath() {
+ return mUri.getPath();
+ }
+
+ InputStream getInputStream() {
+ try {
+ if (mUri.getScheme().equals("file")) {
+ String path = mUri.getPath();
+ if (VERBOSE)
+ Log.v(TAG, "path is " + path);
+ return new java.io.FileInputStream(mUri.getPath());
+ } else {
+ return mContentResolver.openInputStream(mUri);
+ }
+ } catch (FileNotFoundException ex) {
+ return null;
+ }
+ }
+
+ ParcelFileDescriptor getPFD() {
+ try {
+ if (mUri.getScheme().equals("file")) {
+ String path = mUri.getPath();
+ if (VERBOSE)
+ Log.v(TAG, "path is " + path);
+ return ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY);
+ } else {
+ return mContentResolver.openFileDescriptor(mUri, "r");
+ }
+ } catch (FileNotFoundException ex) {
+ return null;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.ImageManager.IImage#fullSizeBitmap(int)
+ */
+ public Bitmap fullSizeBitmap(int targetWidthHeight) {
+ try {
+ ParcelFileDescriptor pfdInput = getPFD();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+
+ if (targetWidthHeight != -1)
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+
+ options.inJustDecodeBounds = false;
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ Bitmap b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ if (VERBOSE) {
+ Log.v(TAG, "B: got bitmap " + b + " with sampleSize " + options.inSampleSize);
+ }
+ pfdInput.close();
+ return b;
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception decoding bitmap " + ex.toString());
+ return null;
+ }
+ }
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthOrHeight) {
+ final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable {
+ ParcelFileDescriptor pfdInput;
+ BitmapFactory.Options mOptions = new BitmapFactory.Options();
+ long mCancelInitiationTime;
+
+ public LoadBitmapCancelable(ParcelFileDescriptor pfd) {
+ pfdInput = pfd;
+ }
+
+ public boolean doCancelWork() {
+ if (VERBOSE)
+ Log.v(TAG, "requesting bitmap load cancel");
+ mCancelInitiationTime = System.currentTimeMillis();
+ mOptions.requestCancelDecode();
+ return true;
+ }
+
+ public Bitmap get() {
+ try {
+ Bitmap b = makeBitmap(targetWidthOrHeight, fullSizeImageUri(), pfdInput, mOptions);
+ if (b == null && mCancelInitiationTime != 0) {
+ if (VERBOSE)
+ Log.v(TAG, "cancel returned null bitmap -- took " + (System.currentTimeMillis()-mCancelInitiationTime));
+ }
+ if (VERBOSE) Log.v(TAG, "b is " + b);
+ return b;
+ } catch (Exception ex) {
+ return null;
+ } finally {
+ acknowledgeCancel();
+ }
+ }
+ }
+
+ try {
+ ParcelFileDescriptor pfdInput = getPFD();
+ if (pfdInput == null)
+ return null;
+ if (VERBOSE) Log.v(TAG, "inputStream is " + pfdInput);
+ return new LoadBitmapCancelable(pfdInput);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+ }
+
+ @Override
+ public Uri fullSizeImageUri() {
+ return mUri;
+ }
+
+ @Override
+ public InputStream fullSizeImageData() {
+ return getInputStream();
+ }
+
+ public long imageId() {
+ return 0;
+ }
+
+ public Bitmap miniThumbBitmap() {
+ return thumbBitmap();
+ }
+
+ @Override
+ public String getTitle() {
+ return mUri.toString();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return getTitle();
+ }
+
+ @Override
+ public String getDescription() {
+ return "";
+ }
+
+ public Bitmap thumbBitmap() {
+ Bitmap b = fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
+ if (b != null) {
+ Matrix m = new Matrix();
+ float scale = Math.min(1F, THUMBNAIL_TARGET_SIZE / (float) b.getWidth());
+ m.setScale(scale, scale);
+ Bitmap scaledBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ return scaledBitmap;
+ } else {
+ return null;
+ }
+ }
+
+ private BitmapFactory.Options snifBitmapOptions() {
+ ParcelFileDescriptor input = getPFD();
+ if (input == null)
+ return null;
+ try {
+ Uri uri = fullSizeImageUri();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ @Override
+ public String getMimeType() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outMimeType : "";
+ }
+
+ @Override
+ public int getHeight() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outHeight : 0;
+ }
+
+ @Override
+ public int getWidth() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outWidth : 0;
+ }
+ }
+
+ public SingleImageList(ContentResolver cr, Uri uri) {
+ super(null, cr, uri, ImageManager.SORT_ASCENDING, null);
+ mContentResolver = cr;
+ mUri = uri;
+ mSingleImage = new UriImage();
+ }
+
+ public HashMap<String, String> getBucketIds() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void deactivate() {
+ // nothing to do here
+ }
+
+ public int getCount() {
+ return 1;
+ }
+
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public IImage getImageAt(int i) {
+ if (i == 0)
+ return mSingleImage;
+
+ return null;
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ if (uri.equals(mUri))
+ return mSingleImage;
+ else
+ return null;
+ }
+
+ public IImage getImageWithId(long id) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected int indexOrientation() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDateTaken() {
+ return -1;
+ }
+
+ @Override
+ protected int indexMimeType() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDescription() {
+ return -1;
+ }
+
+ @Override
+ protected int indexId() {
+ return -1;
+ }
+
+ @Override
+ protected int indexData() {
+ return -1;
+ }
+
+ @Override
+ protected int indexLatitude() {
+ return -1;
+ }
+
+ @Override
+ protected int indexLongitude() {
+ return -1;
+ }
+
+ @Override
+ protected int indexMiniThumbId() {
+ return -1;
+ }
+
+ @Override
+ protected int indexPicasaWeb() {
+ return -1;
+ }
+
+ @Override
+ protected int indexPrivate() {
+ return -1;
+ }
+
+ @Override
+ protected int indexTitle() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDisplayName() {
+ return -1;
+ }
+
+ @Override
+ protected int indexThumbId() {
+ return -1;
+ }
+
+ private InputStream makeInputStream(Uri uri) {
+ InputStream input = null;
+ try {
+ input = mContentResolver.openInputStream(uri);
+ return input;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ @Override
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) {
+ Bitmap b = null;
+
+ try {
+ if (options == null)
+ options = new BitmapFactory.Options();
+ options.inSampleSize = 1;
+
+ if (targetWidthHeight != -1) {
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+ options.inJustDecodeBounds = false;
+ }
+ b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ if (VERBOSE) {
+ Log.v(TAG, "C: got bitmap " + b + " with sampleSize " + options.inSampleSize);
+ }
+ } catch (OutOfMemoryError ex) {
+ if (VERBOSE) Log.v(TAG, "got oom exception " + ex);
+ return null;
+ } finally {
+ try {
+ pfdInput.close();
+ } catch (IOException ex) {
+ }
+ }
+ return b;
+ }
+ }
+
+ class ThreadSafeOutputStream extends OutputStream {
+ java.io.OutputStream mDelegateStream;
+ boolean mClosed;
+
+ public ThreadSafeOutputStream(OutputStream delegate) {
+ mDelegateStream = delegate;
+ }
+
+ @Override
+ synchronized public void close() throws IOException {
+ try {
+ mClosed = true;
+ mDelegateStream.close();
+ } catch (IOException ex) {
+
+ }
+ }
+
+ @Override
+ synchronized public void flush() throws IOException {
+ super.flush();
+ }
+
+ @Override
+ public void write(byte[] b, int offset, int length) throws IOException {
+ /*
+ mDelegateStream.write(b, offset, length);
+ return;
+ */
+ while (length > 0) {
+ synchronized (this) {
+ if (mClosed)
+ return;
+
+ int writeLength = Math.min(8192, length);
+ mDelegateStream.write(b, offset, writeLength);
+ offset += writeLength;
+ length -= writeLength;
+ }
+ }
+ }
+
+ @Override
+ synchronized public void write(int oneByte) throws IOException {
+ if (mClosed)
+ return;
+ mDelegateStream.write(oneByte);
+ }
+ }
+
+ class VideoList extends BaseImageList implements IImageList {
+ private final String[] sProjection = new String[] {
+ Video.Media._ID,
+ Video.Media.DATA,
+ Video.Media.DATE_TAKEN,
+ Video.Media.TITLE,
+ Video.Media.DISPLAY_NAME,
+ Video.Media.DESCRIPTION,
+ Video.Media.IS_PRIVATE,
+ Video.Media.TAGS,
+ Video.Media.CATEGORY,
+ Video.Media.LANGUAGE,
+ Video.Media.LATITUDE,
+ Video.Media.LONGITUDE,
+ Video.Media.MINI_THUMB_MAGIC,
+ Video.Media.MIME_TYPE,
+ };
+
+ final int INDEX_ID = indexOf(sProjection, Video.Media._ID);
+ final int INDEX_DATA = indexOf(sProjection, Video.Media.DATA);
+ final int INDEX_DATE_TAKEN = indexOf(sProjection, Video.Media.DATE_TAKEN);
+ final int INDEX_TITLE = indexOf(sProjection, Video.Media.TITLE);
+ final int INDEX_DISPLAY_NAME = indexOf(sProjection, Video.Media.DISPLAY_NAME);
+ final int INDEX_MIME_TYPE = indexOf(sProjection, Video.Media.MIME_TYPE);
+ final int INDEX_DESCRIPTION = indexOf(sProjection, Video.Media.DESCRIPTION);
+ final int INDEX_PRIVATE = indexOf(sProjection, Video.Media.IS_PRIVATE);
+ final int INDEX_TAGS = indexOf(sProjection, Video.Media.TAGS);
+ final int INDEX_CATEGORY = indexOf(sProjection, Video.Media.CATEGORY);
+ final int INDEX_LANGUAGE = indexOf(sProjection, Video.Media.LANGUAGE);
+ final int INDEX_LATITUDE = indexOf(sProjection, Video.Media.LATITUDE);
+ final int INDEX_LONGITUDE = indexOf(sProjection, Video.Media.LONGITUDE);
+ final int INDEX_MINI_THUMB_MAGIC = indexOf(sProjection, Video.Media.MINI_THUMB_MAGIC);
+ final int INDEX_THUMB_ID = indexOf(sProjection, BaseColumns._ID);
+
+ public VideoList(Context ctx, ContentResolver cr, Uri uri, Uri thumbUri,
+ int sort, String bucketId) {
+ super(ctx, cr, uri, sort, bucketId);
+
+ mCursor = createCursor();
+ if (mCursor == null) {
+ Log.e(TAG, "unable to create video cursor for " + mBaseUri);
+ throw new UnsupportedOperationException();
+ }
+
+ if (Config.LOGV) {
+ Log.v(TAG, "for " + mUri.toString() + " got cursor " + mCursor + " with length "
+ + (mCursor != null ? mCursor.getCount() : -1));
+ }
+
+ if (mCursor == null) {
+ throw new UnsupportedOperationException();
+ }
+ if (mCursor != null && mCursor.moveToFirst()) {
+ int row = 0;
+ do {
+ long imageId = mCursor.getLong(indexId());
+ long dateTaken = mCursor.getLong(indexDateTaken());
+ long miniThumbId = mCursor.getLong(indexMiniThumbId());
+ mCache.put(imageId, new VideoObject(imageId, miniThumbId, mContentResolver,
+ this, dateTaken, row++));
+ } while (mCursor.moveToNext());
+ }
+ }
+
+ public HashMap<String, String> getBucketIds() {
+ Cursor c = Images.Media.query(
+ mContentResolver,
+ mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(),
+ new String[] {
+ VideoColumns.BUCKET_DISPLAY_NAME,
+ VideoColumns.BUCKET_ID
+ },
+ whereClause(),
+ whereClauseArgs(),
+ sortOrder());
+
+ HashMap<String, String> hash = new HashMap<String, String>();
+ if (c != null && c.moveToFirst()) {
+ do {
+ hash.put(c.getString(1), c.getString(0));
+ } while (c.moveToNext());
+ }
+ return hash;
+ }
+
+ protected String whereClause() {
+ if (mBucketId != null) {
+ return Images.Media.BUCKET_ID + " = '" + mBucketId + "'";
+ } else {
+ return null;
+ }
+ }
+
+ protected String[] whereClauseArgs() {
+ return null;
+ }
+
+ @Override
+ protected String thumbnailWhereClause() {
+ return sMiniThumbIsNull;
+ }
+
+ @Override
+ protected String[] thumbnailWhereClauseArgs() {
+ return null;
+ }
+
+ protected Cursor createCursor() {
+ Cursor c =
+ Images.Media.query(
+ mContentResolver,
+ mBaseUri,
+ sProjection,
+ whereClause(),
+ whereClauseArgs(),
+ sortOrder());
+ if (VERBOSE)
+ Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount()));
+ return c;
+ }
+
+ protected int indexOrientation() { return -1; }
+ protected int indexDateTaken() { return INDEX_DATE_TAKEN; }
+ protected int indexDescription() { return INDEX_DESCRIPTION; }
+ protected int indexMimeType() { return INDEX_MIME_TYPE; }
+ protected int indexData() { return INDEX_DATA; }
+ protected int indexId() { return INDEX_ID; }
+ protected int indexLatitude() { return INDEX_LATITUDE; }
+ protected int indexLongitude() { return INDEX_LONGITUDE; }
+ protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; }
+ protected int indexPicasaWeb() { return -1; }
+ protected int indexPrivate() { return INDEX_PRIVATE; }
+ protected int indexTitle() { return INDEX_TITLE; }
+ protected int indexDisplayName() { return -1; }
+ protected int indexThumbId() { return INDEX_THUMB_ID; }
+
+ @Override
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list,
+ long timestamp, int index, int rotation) {
+ return new VideoObject(id, miniThumbId, mContentResolver, this, timestamp, index);
+ }
+
+ @Override
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput,
+ BitmapFactory.Options options) {
+ MediaPlayer mp = new MediaPlayer();
+ Bitmap thumbnail = sDefaultThumbnail;
+ try {
+ mp.setDataSource(mContext, uri);
+// int duration = mp.getDuration();
+// int at = duration > 2000 ? 1000 : duration / 2;
+ int at = 1000;
+ thumbnail = mp.getFrameAt(at);
+ if (Config.LOGV) {
+ if ( thumbnail != null) {
+ Log.v(TAG, "getFrameAt @ " + at + " returned " + thumbnail + "; " +
+ thumbnail.getWidth() + " " + thumbnail.getHeight());
+ } else {
+ Log.v(TAG, "getFrame @ " + at + " failed for " + uri);
+ }
+ }
+ } catch (IOException ex) {
+ } catch (IllegalArgumentException ex) {
+ } catch (SecurityException ex) {
+ } finally {
+ mp.release();
+ }
+ return thumbnail;
+ }
+
+
+ private String sortOrder() {
+ return Video.Media.DATE_TAKEN + (mSort == SORT_ASCENDING ? " ASC " : " DESC");
+ }
+ }
+
+ private final static Bitmap sDefaultThumbnail = Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565);
+
+ /**
+ * Represents a particular video and provides access
+ * to the underlying data and two thumbnail bitmaps
+ * as well as other information such as the id, and
+ * the path to the actual video data.
+ */
+ class VideoObject extends BaseImage implements IImage {
+ /**
+ * Constructor.
+ *
+ * @param id the image id of the image
+ * @param cr the content resolver
+ */
+ protected VideoObject(long id, long miniThumbId, ContentResolver cr, VideoList container,
+ long dateTaken, int row) {
+ super(id, miniThumbId, cr, container, row);
+ }
+
+ protected Bitmap.CompressFormat compressionType() {
+ return Bitmap.CompressFormat.JPEG;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null)
+ return false;
+ if (!(other instanceof VideoObject))
+ return false;
+
+ return fullSizeImageUri().equals(((VideoObject)other).fullSizeImageUri());
+ }
+
+ public String getDataPath() {
+ String path = null;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ int column = ((VideoList)getContainer()).indexData();
+ if (column >= 0)
+ path = c.getString(column);
+ }
+ }
+ return path;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#fullSizeBitmap()
+ */
+ public Bitmap fullSizeBitmap(int targetWidthHeight) {
+ return sNoImageBitmap;
+ }
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthHeight) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#fullSizeImageData()
+ */
+ public InputStream fullSizeImageData() {
+ try {
+ InputStream input = mContentResolver.openInputStream(
+ fullSizeImageUri());
+ return input;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#fullSizeImageId()
+ */
+ public long fullSizeImageId() {
+ return mId;
+ }
+
+ public String getCategory() {
+ return getStringEntry(((VideoList)mContainer).INDEX_CATEGORY);
+ }
+
+ public int getHeight() {
+ return 0;
+ }
+
+ public String getLanguage() {
+ return getStringEntry(((VideoList)mContainer).INDEX_LANGUAGE);
+ }
+
+ public String getPicasaId() {
+ return null;
+ }
+
+ private String getStringEntry(int entryName) {
+ String entry = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ entry = c.getString(entryName);
+ }
+ }
+ return entry;
+ }
+
+ public String getTags() {
+ return getStringEntry(((VideoList)mContainer).INDEX_TAGS);
+ }
+
+ public int getWidth() {
+ return 0;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#imageId()
+ */
+ public long imageId() {
+ return mId;
+ }
+
+ public boolean isReadonly() {
+ return false;
+ }
+
+ public boolean isDrm() {
+ return false;
+ }
+
+ public boolean rotateImageBy(int degrees) {
+ return false;
+ }
+
+ public void setCategory(String category) {
+ setStringEntry(category, ((VideoList)mContainer).INDEX_CATEGORY);
+ }
+
+ public void setLanguage(String language) {
+ setStringEntry(language, ((VideoList)mContainer).INDEX_LANGUAGE);
+ }
+
+ private void setStringEntry(String entry, int entryName) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateString(entryName, entry);
+ }
+ }
+ }
+
+ public void setTags(String tags) {
+ setStringEntry(tags, ((VideoList)mContainer).INDEX_TAGS);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#thumb1()
+ */
+ public Bitmap thumbBitmap() {
+ return fullSizeBitmap(320);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("" + mId);
+ return sb.toString();
+ }
+ }
+
+ private final static Bitmap sNoImageBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
+
+ /*
+ * How much quality to use when storing the thumbnail.
+ */
+ private static ImageManager sInstance = null;
+ private static final int MINI_THUMB_TARGET_SIZE = 96;
+ private static final int THUMBNAIL_TARGET_SIZE = 320;
+
+ private static final String[] THUMB_PROJECTION = new String[] {
+ BaseColumns._ID, // 0
+ Images.Thumbnails.IMAGE_ID, // 1
+ Images.Thumbnails.WIDTH,
+ Images.Thumbnails.HEIGHT
+ };
+
+ private static Uri sStorageURI = Images.Media.EXTERNAL_CONTENT_URI;
+
+ private static Uri sThumbURI = Images.Thumbnails.EXTERNAL_CONTENT_URI;
+
+ private static Uri sVideoStorageURI = Uri.parse("content://media/external/video/media");
+
+ private static Uri sVideoThumbURI = Uri.parse("content://media/external/video/thumbnails");
+ /**
+ * Returns an ImageList object that contains
+ * all of the images.
+ * @param cr
+ * @param location
+ * @param includeImages
+ * @param includeVideo
+ * @return the singleton ImageList
+ */
+ static final public int SORT_ASCENDING = 1;
+
+ static final public int SORT_DESCENDING = 2;
+
+ static final public int INCLUDE_IMAGES = (1 << 0);
+ static final public int INCLUDE_DRM_IMAGES = (1 << 1);
+ static final public int INCLUDE_VIDEOS = (1 << 2);
+
+ static public DataLocation getDefaultDataLocation() {
+ return DataLocation.EXTERNAL;
+ }
+ private static int indexOf(String [] array, String s) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i].equals(s)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the singleton instance of the ImageManager.
+ * @return the ImageManager instance.
+ */
+ public static ImageManager instance() {
+ if (sInstance == null) {
+ sInstance = new ImageManager();
+ }
+ return sInstance;
+ }
+
+ /**
+ * Creates a byte[] for a given bitmap of the desired size. Recycles the input bitmap.
+ */
+ static public byte[] miniThumbData(Bitmap source) {
+ if (source == null)
+ return null;
+
+ Bitmap miniThumbnail = extractMiniThumb(source, MINI_THUMB_TARGET_SIZE,
+ MINI_THUMB_TARGET_SIZE);
+ java.io.ByteArrayOutputStream miniOutStream = new java.io.ByteArrayOutputStream();
+ miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
+ miniThumbnail.recycle();
+
+ try {
+ miniOutStream.close();
+ byte [] data = miniOutStream.toByteArray();
+ return data;
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "got exception ex " + ex);
+ }
+ return null;
+ }
+
+ /**
+ * Creates a centered bitmap of the desired size. Recycles the input.
+ * @param source
+ * @return
+ */
+ static public Bitmap extractMiniThumb(Bitmap source, int width, int height) {
+ if (source == null) {
+ return null;
+ }
+
+ float scale;
+ if (source.getWidth() < source.getHeight()) {
+ scale = width / (float)source.getWidth();
+ } else {
+ scale = height / (float)source.getHeight();
+ }
+ Matrix matrix = new Matrix();
+ matrix.setScale(scale, scale);
+ Bitmap miniThumbnail = ImageLoader.transform(matrix, source,
+ width, height, false);
+
+ if (miniThumbnail != source) {
+ source.recycle();
+ }
+ return miniThumbnail;
+ }
+
+ static Bitmap rotate(Bitmap b, int degrees) {
+ if (degrees != 0 && b != null) {
+ Matrix m = new Matrix();
+ m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
+
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ // TODO should recycle here but that needs more testing/verification
+// b.recycle();
+ b = b2;
+ }
+ return b;
+ }
+
+ public static int roundOrientation(int orientationInput) {
+ int orientation = orientationInput;
+ if (orientation == -1)
+ orientation = 0;
+
+ orientation = orientation % 360;
+ int retVal;
+ if (orientation < (0*90) + 45) {
+ retVal = 0;
+ } else if (orientation < (1*90) + 45) {
+ retVal = 90;
+ } else if (orientation < (2*90) + 45) {
+ retVal = 180;
+ } else if (orientation < (3*90) + 45) {
+ retVal = 270;
+ } else {
+ retVal = 0;
+ }
+
+ if (VERBOSE) Log.v(TAG, "map orientation " + orientationInput + " to " + retVal);
+ return retVal;
+ }
+
+
+ /**
+ * @return true if the mimetype is an image mimetype.
+ */
+ public static boolean isImageMimeType(String mimeType) {
+ return mimeType.startsWith("image/");
+ }
+
+ /**
+ * @return true if the mimetype is a video mimetype.
+ */
+ public static boolean isVideoMimeType(String mimeType) {
+ return mimeType.startsWith("video/");
+ }
+
+ /**
+ * @return true if the image is an image.
+ */
+ public static boolean isImage(IImage image) {
+ return isImageMimeType(image.getMimeType());
+ }
+
+ /**
+ * @return true if the image is a video.
+ */
+ public static boolean isVideo(IImage image) {
+ return isVideoMimeType(image.getMimeType());
+ }
+
+ public Uri addImage(
+ final Context ctx,
+ final ContentResolver cr,
+ final String imageName,
+ final String description,
+ final long dateTaken,
+ final Location location,
+ final int orientation,
+ final String directory,
+ final String filename) {
+ ContentValues values = new ContentValues(7);
+ values.put(Images.Media.TITLE, imageName);
+ values.put(Images.Media.DISPLAY_NAME, imageName);
+ values.put(Images.Media.DESCRIPTION, description);
+ values.put(Images.Media.DATE_TAKEN, dateTaken);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.ORIENTATION, orientation);
+
+ File parentFile = new File(directory);
+ // Lowercase the path for hashing. This avoids duplicate buckets if the filepath
+ // case is changed externally.
+ // Keep the original case for display.
+ String path = parentFile.toString().toLowerCase();
+ String name = parentFile.getName();
+
+ if (VERBOSE) Log.v(TAG, "addImage id is " + path.hashCode() + "; name " + name + "; path is " + path);
+
+ if (location != null) {
+ if (VERBOSE) {
+ Log.v(TAG, "lat long " + location.getLatitude() + " / " + location.getLongitude());
+ }
+ values.put(Images.Media.LATITUDE, location.getLatitude());
+ values.put(Images.Media.LONGITUDE, location.getLongitude());
+ }
+
+ if (directory != null && filename != null) {
+ String value = directory + "/" + filename;
+ values.put("_data", value);
+ }
+
+ long t3 = System.currentTimeMillis();
+ Uri uri = cr.insert(sStorageURI, values);
+
+ // The line above will create a filename that ends in .jpg
+ // That filename is what will be handed to gmail when a user shares a photo.
+ // Gmail gets the name of the picture attachment from the "DISPLAY_NAME" field.
+ // Extract the filename and jam it into the display name.
+ Cursor c = cr.query(
+ uri,
+ new String [] { ImageColumns._ID, Images.Media.DISPLAY_NAME, "_data" },
+ null,
+ null,
+ null);
+ if (c.moveToFirst()) {
+ String filePath = c.getString(2);
+ if (filePath != null) {
+ int pos = filePath.lastIndexOf("/");
+ if (pos >= 0) {
+ filePath = filePath.substring(pos + 1); // pick off the filename
+ c.updateString(1, filePath);
+ c.commitUpdates();
+ }
+ }
+ }
+ c.close();
+ return uri;
+ }
+
+ public IAddImage_cancelable storeImage(
+ final Uri uri,
+ final Context ctx,
+ final ContentResolver cr,
+ final int orientation,
+ final Bitmap source,
+ final byte [] jpegData) {
+ class AddImageCancelable extends BaseCancelable implements IAddImage_cancelable {
+ private IGetBoolean_cancelable mSaveImageCancelable;
+
+ public boolean doCancelWork() {
+ if (VERBOSE) {
+ Log.v(TAG, "calling AddImageCancelable.cancel() " + mSaveImageCancelable);
+ }
+
+ if (mSaveImageCancelable != null) {
+ mSaveImageCancelable.cancel();
+ }
+ return true;
+ }
+
+ public void get() {
+ if (source == null && jpegData == null) {
+ throw new IllegalArgumentException("source cannot be null");
+ }
+
+ try {
+ long t1 = System.currentTimeMillis();
+ synchronized (this) {
+ if (mCancel) {
+ throw new CanceledException();
+ }
+ }
+ long id = ContentUris.parseId(uri);
+
+ BaseImageList il = new ImageList(ctx, cr, sStorageURI, sThumbURI, SORT_ASCENDING, null);
+ ImageManager.Image image = new Image(id, 0, cr, il, il.getCount(), 0);
+ long t5 = System.currentTimeMillis();
+ Cursor c = cr.query(
+ uri,
+ new String [] { ImageColumns._ID, ImageColumns.MINI_THUMB_MAGIC, "_data" },
+ null,
+ null,
+ null);
+ c.moveToPosition(0);
+
+ synchronized (this) {
+ checkCanceled();
+ mSaveImageCancelable = image.saveImageContents(source, jpegData, orientation, true, c);
+ }
+
+ if (mSaveImageCancelable.get()) {
+ long t6 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "saveImageContents took " + (t6-t5));
+ if (VERBOSE) Log.v(TAG, "updating new picture with id " + id);
+ c.updateLong(1, id);
+ c.commitUpdates();
+ c.close();
+ long t7 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "commit updates to save mini thumb took " + (t7-t6));
+ }
+ else {
+ c.close();
+ throw new CanceledException();
+ }
+ } catch (CanceledException ex) {
+ if (VERBOSE) {
+ Log.v(TAG, "caught CanceledException");
+ }
+ if (uri != null) {
+ if (VERBOSE) {
+ Log.v(TAG, "canceled... cleaning up this uri: " + uri);
+ }
+ cr.delete(uri, null, null);
+ }
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new AddImageCancelable();
+ }
+
+ static public IImageList makeImageList(Uri uri, Context ctx, int sort) {
+ ContentResolver cr = ctx.getContentResolver();
+ String uriString = (uri != null) ? uri.toString() : "";
+ // TODO we need to figure out whether we're viewing
+ // DRM images in a better way. Is there a constant
+ // for content://drm somewhere??
+ IImageList imageList;
+
+ if (uriString.startsWith("content://drm")) {
+ imageList = ImageManager.instance().allImages(
+ ctx,
+ cr,
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_DRM_IMAGES,
+ sort);
+ } else if (!uriString.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
+ && !uriString.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString())) {
+ imageList = ImageManager.instance().new SingleImageList(cr, uri);
+ } else {
+ String bucketId = uri.getQueryParameter("bucketId");
+ if (VERBOSE) Log.v(TAG, "bucketId is " + bucketId);
+ imageList = ImageManager.instance().allImages(
+ ctx,
+ cr,
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ sort,
+ bucketId);
+ }
+ return imageList;
+ }
+
+ public IImageList emptyImageList() {
+ return
+ new IImageList() {
+ public void checkThumbnails(ImageManager.IImageList.ThumbCheckCallback cb,
+ int totalThumbnails) {
+ }
+
+ public void commitChanges() {
+ }
+
+ public void deactivate() {
+ }
+
+ public HashMap<String, String> getBucketIds() {
+ return new HashMap<String,String>();
+ }
+
+ public int getCount() {
+ return 0;
+ }
+
+ public boolean isEmpty() {
+ return true;
+ }
+
+ public IImage getImageAt(int i) {
+ return null;
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ return null;
+ }
+
+ public boolean removeImage(IImage image) {
+ return false;
+ }
+
+ public void removeImageAt(int i) {
+ }
+
+ public void removeOnChangeListener(ImageManager.IImageList.OnChange changeCallback) {
+ }
+
+ public void setOnChangeListener(ImageManager.IImageList.OnChange changeCallback,
+ Handler h) {
+ }
+
+ };
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort) {
+ return allImages(ctx, cr, location, inclusion, sort, null, null);
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId) {
+ return allImages(ctx, cr, location, inclusion, sort, bucketId, null);
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId, Uri specificImageUri) {
+ if (VERBOSE) {
+ Log.v(TAG, "allImages " + location + " " + ((inclusion&INCLUDE_IMAGES)!=0) + " + v=" + ((inclusion&INCLUDE_VIDEOS)!=0));
+ }
+
+ if (cr == null) {
+ return null;
+ } else {
+ // false ==> don't require write access
+ boolean haveSdCard = hasStorage(false);
+
+ if (true) {
+ // use this code to merge videos and stills into the same list
+ ArrayList<IImageList> l = new ArrayList<IImageList>();
+
+ if (VERBOSE) {
+ Log.v(TAG, "initializing ... haveSdCard == " + haveSdCard + "; inclusion is " + String.format("%x", inclusion));
+ }
+ if (specificImageUri != null) {
+ try {
+ if (specificImageUri.getScheme().equalsIgnoreCase("content"))
+ l.add(new ImageList(ctx, cr, specificImageUri, sThumbURI, sort, bucketId));
+ else
+ l.add(new SingleImageList(cr, specificImageUri));
+ } catch (UnsupportedOperationException ex) {
+ }
+ } else {
+ if (haveSdCard && location != DataLocation.INTERNAL) {
+ if ((inclusion & INCLUDE_IMAGES) != 0) {
+ try {
+ l.add(new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ if ((inclusion & INCLUDE_VIDEOS) != 0) {
+ try {
+ l.add(new VideoList(ctx, cr, sVideoStorageURI, sVideoThumbURI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ }
+ if (location == DataLocation.INTERNAL || location == DataLocation.ALL) {
+ if ((inclusion & INCLUDE_IMAGES) != 0) {
+ try {
+ l.add(new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI,
+ Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ if ((inclusion & INCLUDE_DRM_IMAGES) != 0) {
+ try {
+ l.add(new DrmImageList(ctx, cr, DrmStore.Images.CONTENT_URI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ }
+ }
+
+ IImageList [] imageList = l.toArray(new IImageList[l.size()]);
+ return new ImageListUber(imageList, sort);
+ } else {
+ if (haveSdCard && location != DataLocation.INTERNAL) {
+ return new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId);
+ } else {
+ return new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI,
+ Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId);
+ }
+ }
+ }
+ }
+
+ // Create a temporary file to see whether a volume is really writeable. It's important not to
+ // put it in the root directory which may have a limit on the number of files.
+ static private boolean checkFsWritable() {
+ String directoryName = Environment.getExternalStorageDirectory().toString() + "/DCIM";
+ File directory = new File(directoryName);
+ if (!directory.isDirectory()) {
+ if (!directory.mkdirs()) {
+ return false;
+ }
+ }
+ File f = new File(directoryName, ".probe");
+ try {
+ // Remove stale file if any
+ if (f.exists()) {
+ f.delete();
+ }
+ if (!f.createNewFile())
+ return false;
+ f.delete();
+ return true;
+ } catch (IOException ex) {
+ return false;
+ }
+ }
+
+ static public boolean hasStorage() {
+ return hasStorage(true);
+ }
+
+ static public boolean hasStorage(boolean requireWriteAccess) {
+ String state = Environment.getExternalStorageState();
+ if (VERBOSE) Log.v(TAG, "state is " + state);
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ if (requireWriteAccess) {
+ boolean writable = checkFsWritable();
+ if (VERBOSE) Log.v(TAG, "writable is " + writable);
+ return writable;
+ } else {
+ return true;
+ }
+ } else if (!requireWriteAccess && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+ return true;
+ }
+ return false;
+ }
+
+ public static Cursor query(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ if (resolver == null) {
+ return null;
+ }
+ return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+
+ }
+
+ public static boolean isMediaScannerScanning(Context context) {
+ boolean result = false;
+ Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
+ new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
+ if (cursor != null) {
+ if (cursor.getCount() == 1) {
+ cursor.moveToFirst();
+ result = "external".equals(cursor.getString(0));
+ }
+ cursor.close();
+ }
+
+ if (VERBOSE)
+ Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>>>>>> isMediaScannerScanning returning " + result);
+ return result;
+ }
+
+ /**
+ * Create a video thumbnail for a video. May return null if the video is corrupt.
+ * @param filePath
+ * @return
+ */
+ public static Bitmap createVideoThumbnail(String filePath) {
+ Bitmap bitmap = null;
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ try {
+ retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY);
+ retriever.setDataSource(filePath);
+ bitmap = retriever.captureFrame();
+ } catch(IllegalArgumentException ex) {
+ // Assume this is a corrupt video file
+ } catch (RuntimeException ex) {
+ // Assume this is a corrupt video file.
+ } finally {
+ try {
+ retriever.release();
+ } catch (RuntimeException ex) {
+ // Ignore failures while cleaning up.
+ }
+ }
+ return bitmap;
+ }
+
+ public static String getLastThumbPath() {
+ return Environment.getExternalStorageDirectory().toString() +
+ "/DCIM/.thumbnails/camera_last_thumb";
+ }
+}
diff --git a/src/com/android/camera/ImageViewTouchBase.java b/src/com/android/camera/ImageViewTouchBase.java
new file mode 100644
index 0000000..1774e46
--- /dev/null
+++ b/src/com/android/camera/ImageViewTouchBase.java
@@ -0,0 +1,559 @@
+package com.android.camera;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+import android.view.KeyEvent;
+import android.widget.ImageView;
+
+abstract public class ImageViewTouchBase extends ImageView {
+ private static final String TAG = "ImageViewTouchBase";
+
+ // if we're animating these images, it may be faster to cache the image
+ // at its target size first. to do this set this variable to true.
+ // currently we're not animating images, so we don't need to do this
+ // extra work.
+ private final boolean USE_PERFECT_FIT_OPTIMIZATION = false;
+
+ // This is the base transformation which is used to show the image
+ // initially. The current computation for this shows the image in
+ // it's entirety, letterboxing as needed. One could chose to
+ // show the image as cropped instead.
+ //
+ // This matrix is recomputed when we go from the thumbnail image to
+ // the full size image.
+ protected Matrix mBaseMatrix = new Matrix();
+
+ // This is the supplementary transformation which reflects what
+ // the user has done in terms of zooming and panning.
+ //
+ // This matrix remains the same when we go from the thumbnail image
+ // to the full size image.
+ protected Matrix mSuppMatrix = new Matrix();
+
+ // This is the final matrix which is computed as the concatentation
+ // of the base matrix and the supplementary matrix.
+ private Matrix mDisplayMatrix = new Matrix();
+
+ // Temporary buffer used for getting the values out of a matrix.
+ private float[] mMatrixValues = new float[9];
+
+ // The current bitmap being displayed.
+ protected Bitmap mBitmapDisplayed;
+
+ // The thumbnail bitmap.
+ protected Bitmap mThumbBitmap;
+
+ // The full size bitmap which should be used once we start zooming.
+ private Bitmap mFullBitmap;
+
+ // The bitmap which is exactly sized to what we need. The decoded bitmap is
+ // drawn into the mPerfectFitBitmap so that animation is faster.
+ protected Bitmap mPerfectFitBitmap;
+
+ // True if the image is the thumbnail.
+ protected boolean mBitmapIsThumbnail;
+
+ // True if the user is zooming -- use the full size image
+ protected boolean mIsZooming;
+
+ // Paint to use to clear the "mPerfectFitBitmap"
+ protected Paint mPaint = new Paint();
+
+ static boolean sNewZoomControl = false;
+
+ int mThisWidth = -1, mThisHeight = -1;
+
+ float mMaxZoom;
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mThisWidth = right - left;
+ mThisHeight = bottom - top;
+ Runnable r = mOnLayoutRunnable;
+ if (r != null) {
+ mOnLayoutRunnable = null;
+ r.run();
+ }
+ if (mBitmapDisplayed != null) {
+ setBaseMatrix(mBitmapDisplayed, mBaseMatrix);
+ setImageMatrix(getImageViewMatrix());
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && getScale() > 1.0f) {
+ // If we're zoomed in, pressing Back jumps out to show the entire image, otherwise Back
+ // returns the user to the gallery.
+ zoomTo(1.0f);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ protected Handler mHandler = new Handler();
+
+ protected int mLastXTouchPos;
+ protected int mLastYTouchPos;
+
+ protected boolean doesScrolling() {
+ return true;
+ }
+
+ // Translate a given point through a given matrix.
+ static private void translatePoint(Matrix matrix, float [] xy) {
+ matrix.mapPoints(xy);
+ }
+
+ // Return the mapped x coordinate through the matrix.
+ static int mapXPoint(Matrix matrix, int point) {
+ // Matrix's mapPoints takes an array of x/y coordinates.
+ // That's why we have to allocte an array of length two
+ // even though we don't use the y coordinate.
+ float [] xy = new float[2];
+ xy[0] = point;
+ xy[1] = 0F;
+ matrix.mapPoints(xy);
+ return (int) xy[0];
+ }
+
+ @Override
+ public void setImageBitmap(Bitmap bitmap) {
+ throw new NullPointerException();
+ }
+
+ public void setImageBitmap(Bitmap bitmap, boolean isThumbnail) {
+ super.setImageBitmap(bitmap);
+ Drawable d = getDrawable();
+ if (d != null)
+ d.setDither(true);
+ mBitmapDisplayed = bitmap;
+ mBitmapIsThumbnail = isThumbnail;
+ }
+
+ protected boolean usePerfectFitBitmap() {
+ return USE_PERFECT_FIT_OPTIMIZATION && !mIsZooming;
+ }
+
+ public void recycleBitmaps() {
+ if (mFullBitmap != null) {
+ if (Config.LOGV)
+ Log.v(TAG, "recycling mFullBitmap " + mFullBitmap + "; this == " + this.hashCode());
+ mFullBitmap.recycle();
+ mFullBitmap = null;
+ }
+ if (mThumbBitmap != null) {
+ if (Config.LOGV)
+ Log.v(TAG, "recycling mThumbBitmap" + mThumbBitmap + "; this == " + this.hashCode());
+ mThumbBitmap.recycle();
+ mThumbBitmap = null;
+ }
+
+ // mBitmapDisplayed is either mPerfectFitBitmap or mFullBitmap (in the case of zooming)
+ setImageBitmap(null, true);
+ }
+
+ public void clear() {
+ mBitmapDisplayed = null;
+ recycleBitmaps();
+ }
+
+ private Runnable mOnLayoutRunnable = null;
+
+ public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp, final boolean isThumb) {
+ if ((bitmap != null) && (bitmap == mPerfectFitBitmap)) {
+ // TODO: this should be removed in production
+ throw new IllegalArgumentException("bitmap must not be mPerfectFitBitmap");
+ }
+
+ final int viewWidth = getWidth();
+ final int viewHeight = getHeight();
+
+ if (viewWidth <= 0) {
+ mOnLayoutRunnable = new Runnable() {
+ public void run() {
+ setImageBitmapResetBase(bitmap, resetSupp, isThumb);
+ }
+ };
+ return;
+ }
+
+ if (isThumb && mThumbBitmap != bitmap) {
+ if (mThumbBitmap != null) {
+ mThumbBitmap.recycle();
+ }
+ mThumbBitmap = bitmap;
+ } else if (!isThumb && mFullBitmap != bitmap) {
+ if (mFullBitmap != null) {
+ mFullBitmap.recycle();
+ }
+ mFullBitmap = bitmap;
+ }
+ mBitmapIsThumbnail = isThumb;
+
+ if (bitmap != null) {
+ if (!usePerfectFitBitmap()) {
+ setScaleType(ImageView.ScaleType.MATRIX);
+ setBaseMatrix(bitmap, mBaseMatrix);
+ setImageBitmap(bitmap, isThumb);
+ } else {
+ Matrix matrix = new Matrix();
+ setBaseMatrix(bitmap, matrix);
+ if ((mPerfectFitBitmap == null) ||
+ mPerfectFitBitmap.getWidth() != mThisWidth ||
+ mPerfectFitBitmap.getHeight() != mThisHeight) {
+ if (mPerfectFitBitmap != null) {
+ if (Config.LOGV)
+ Log.v(TAG, "recycling mPerfectFitBitmap " + mPerfectFitBitmap.hashCode());
+ mPerfectFitBitmap.recycle();
+ }
+ mPerfectFitBitmap = Bitmap.createBitmap(mThisWidth, mThisHeight, Bitmap.Config.RGB_565);
+ }
+ Canvas canvas = new Canvas(mPerfectFitBitmap);
+ // clear the bitmap which may be bigger than the image and
+ // contain the the previous image.
+ canvas.drawColor(0xFF000000);
+
+ final int bw = bitmap.getWidth();
+ final int bh = bitmap.getHeight();
+ final float widthScale = Math.min(viewWidth / (float)bw, 1.0f);
+ final float heightScale = Math.min(viewHeight/ (float)bh, 1.0f);
+ int translateX, translateY;
+ if (widthScale > heightScale) {
+ translateX = (int)((viewWidth -(float)bw*heightScale)*0.5f);
+ translateY = (int)((viewHeight-(float)bh*heightScale)*0.5f);
+ } else {
+ translateX = (int)((viewWidth -(float)bw*widthScale)*0.5f);
+ translateY = (int)((viewHeight-(float)bh*widthScale)*0.5f);
+ }
+
+ android.graphics.Rect src = new android.graphics.Rect(0, 0, bw, bh);
+ android.graphics.Rect dst = new android.graphics.Rect(
+ translateX, translateY,
+ mThisWidth - translateX, mThisHeight - translateY);
+ canvas.drawBitmap(bitmap, src, dst, mPaint);
+
+ setImageBitmap(mPerfectFitBitmap, isThumb);
+ setScaleType(ImageView.ScaleType.MATRIX);
+ setImageMatrix(null);
+ }
+ } else {
+ mBaseMatrix.reset();
+ setImageBitmap(null, isThumb);
+ }
+
+ if (resetSupp)
+ mSuppMatrix.reset();
+ setImageMatrix(getImageViewMatrix());
+ mMaxZoom = maxZoom();
+ }
+
+ // Center as much as possible in one or both axis. Centering is
+ // defined as follows: if the image is scaled down below the
+ // view's dimensions then center it (literally). If the image
+ // is scaled larger than the view and is translated out of view
+ // then translate it back into view (i.e. eliminate black bars).
+ protected void center(boolean vertical, boolean horizontal, boolean animate) {
+ if (mBitmapDisplayed == null)
+ return;
+
+ Matrix m = getImageViewMatrix();
+
+ float [] topLeft = new float[] { 0, 0 };
+ float [] botRight = new float[] { mBitmapDisplayed.getWidth(), mBitmapDisplayed.getHeight() };
+
+ translatePoint(m, topLeft);
+ translatePoint(m, botRight);
+
+ float height = botRight[1] - topLeft[1];
+ float width = botRight[0] - topLeft[0];
+
+ float deltaX = 0, deltaY = 0;
+
+ if (vertical) {
+ int viewHeight = getHeight();
+ if (height < viewHeight) {
+ deltaY = (viewHeight - height)/2 - topLeft[1];
+ } else if (topLeft[1] > 0) {
+ deltaY = -topLeft[1];
+ } else if (botRight[1] < viewHeight) {
+ deltaY = getHeight() - botRight[1];
+ }
+ }
+
+ if (horizontal) {
+ int viewWidth = getWidth();
+ if (width < viewWidth) {
+ deltaX = (viewWidth - width)/2 - topLeft[0];
+ } else if (topLeft[0] > 0) {
+ deltaX = -topLeft[0];
+ } else if (botRight[0] < viewWidth) {
+ deltaX = viewWidth - botRight[0];
+ }
+ }
+
+ postTranslate(deltaX, deltaY);
+ if (animate) {
+ Animation a = new TranslateAnimation(-deltaX, 0, -deltaY, 0);
+ a.setStartTime(SystemClock.elapsedRealtime());
+ a.setDuration(250);
+ setAnimation(a);
+ }
+ setImageMatrix(getImageViewMatrix());
+ }
+
+ public void copyFrom(ImageViewTouchBase other) {
+ mSuppMatrix.set(other.mSuppMatrix);
+ mBaseMatrix.set(other.mBaseMatrix);
+
+ if (mThumbBitmap != null)
+ mThumbBitmap.recycle();
+
+ if (mFullBitmap != null)
+ mFullBitmap.recycle();
+
+ // copy the data
+ mThumbBitmap = other.mThumbBitmap;
+ mFullBitmap = null;
+
+ if (other.mFullBitmap != null)
+ other.mFullBitmap.recycle();
+
+ // transfer "ownership"
+ other.mThumbBitmap = null;
+ other.mFullBitmap = null;
+ other.mBitmapIsThumbnail = true;
+
+ setImageMatrix(other.getImageMatrix());
+ setScaleType(other.getScaleType());
+
+ setImageBitmapResetBase(mThumbBitmap, true, true);
+ }
+
+ @Override
+ public void setImageDrawable(android.graphics.drawable.Drawable d) {
+ super.setImageDrawable(d);
+ }
+
+ public ImageViewTouchBase(Context context) {
+ super(context);
+ init();
+ }
+
+ public ImageViewTouchBase(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ setScaleType(ImageView.ScaleType.MATRIX);
+ mPaint.setDither(true);
+ mPaint.setFilterBitmap(true);
+ }
+
+ protected float getValue(Matrix matrix, int whichValue) {
+ matrix.getValues(mMatrixValues);
+ return mMatrixValues[whichValue];
+ }
+
+ // Get the scale factor out of the matrix.
+ protected float getScale(Matrix matrix) {
+ return getValue(matrix, Matrix.MSCALE_X);
+ }
+
+ protected float getScale() {
+ return getScale(mSuppMatrix);
+ }
+
+ protected float getTranslateX() {
+ return getValue(mSuppMatrix, Matrix.MTRANS_X);
+ }
+
+ protected float getTranslateY() {
+ return getValue(mSuppMatrix, Matrix.MTRANS_Y);
+ }
+
+ // Setup the base matrix so that the image is centered and scaled properly.
+ private void setBaseMatrix(Bitmap bitmap, Matrix matrix) {
+ float viewWidth = getWidth();
+ float viewHeight = getHeight();
+
+ matrix.reset();
+ float widthScale = Math.min(viewWidth / (float)bitmap.getWidth(), 1.0f);
+ float heightScale = Math.min(viewHeight / (float)bitmap.getHeight(), 1.0f);
+ float scale;
+ if (widthScale > heightScale) {
+ scale = heightScale;
+ } else {
+ scale = widthScale;
+ }
+ matrix.setScale(scale, scale);
+ matrix.postTranslate(
+ (viewWidth - ((float)bitmap.getWidth() * scale))/2F,
+ (viewHeight - ((float)bitmap.getHeight() * scale))/2F);
+ }
+
+ // Combine the base matrix and the supp matrix to make the final matrix.
+ protected Matrix getImageViewMatrix() {
+ mDisplayMatrix.set(mBaseMatrix);
+ mDisplayMatrix.postConcat(mSuppMatrix);
+ return mDisplayMatrix;
+ }
+
+ private void onZoom() {
+ mIsZooming = true;
+ if (mFullBitmap != null && mFullBitmap != mBitmapDisplayed) {
+ setImageBitmapResetBase(mFullBitmap, false, mBitmapIsThumbnail);
+ }
+ }
+
+ private String describe(Bitmap b) {
+ StringBuilder sb = new StringBuilder();
+ if (b == null) {
+ sb.append("NULL");
+ } else if (b.isRecycled()) {
+ sb.append(String.format("%08x: RECYCLED", b.hashCode()));
+ } else {
+ sb.append(String.format("%08x: LIVE", b.hashCode()));
+ sb.append(String.format("%d x %d (size == %d)", b.getWidth(), b.getHeight(), b.getWidth()*b.getHeight()*2));
+ }
+ return sb.toString();
+ }
+
+ public void dump() {
+ if (Config.LOGV) {
+ Log.v(TAG, "dump ImageViewTouchBase " + this);
+ Log.v(TAG, "... mBitmapDisplayed = " + describe(mBitmapDisplayed));
+ Log.v(TAG, "... mThumbBitmap = " + describe(mThumbBitmap));
+ Log.v(TAG, "... mFullBitmap = " + describe(mFullBitmap));
+ Log.v(TAG, "... mPerfectFitBitmap = " + describe(mPerfectFitBitmap));
+ Log.v(TAG, "... mIsThumb = " + mBitmapIsThumbnail);
+ }
+ }
+
+ static final float sPanRate = 7;
+ static final float sScaleRate = 1.25F;
+
+ // Sets the maximum zoom, which is a scale relative to the base matrix. It is calculated to show
+ // the image at 400% zoom regardless of screen or image orientation. If in the future we decode
+ // the full 3 megapixel image, rather than the current 1024x768, this should be changed down to
+ // 200%.
+ protected float maxZoom() {
+ if (mBitmapDisplayed == null)
+ return 1F;
+
+ float fw = (float) mBitmapDisplayed.getWidth() / (float)mThisWidth;
+ float fh = (float) mBitmapDisplayed.getHeight() / (float)mThisHeight;
+ float max = Math.max(fw, fh) * 4;
+// Log.v(TAG, "Bitmap " + mBitmapDisplayed.getWidth() + "x" + mBitmapDisplayed.getHeight() +
+// " view " + mThisWidth + "x" + mThisHeight + " max zoom " + max);
+ return max;
+ }
+
+ protected void zoomTo(float scale, float centerX, float centerY) {
+ if (scale > mMaxZoom) {
+ scale = mMaxZoom;
+ }
+ onZoom();
+
+ float oldScale = getScale();
+ float deltaScale = scale / oldScale;
+
+ mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
+ setImageMatrix(getImageViewMatrix());
+ center(true, true, false);
+ }
+
+ protected void zoomTo(final float scale, final float centerX, final float centerY, final float durationMs) {
+ final float incrementPerMs = (scale - getScale()) / durationMs;
+ final float oldScale = getScale();
+ final long startTime = System.currentTimeMillis();
+
+ mHandler.post(new Runnable() {
+ public void run() {
+ long now = System.currentTimeMillis();
+ float currentMs = Math.min(durationMs, (float)(now - startTime));
+ float target = oldScale + (incrementPerMs * currentMs);
+ zoomTo(target, centerX, centerY);
+
+ if (currentMs < durationMs) {
+ mHandler.post(this);
+ }
+ }
+ });
+ }
+
+ protected void zoomTo(float scale) {
+ float width = getWidth();
+ float height = getHeight();
+
+ zoomTo(scale, width/2F, height/2F);
+ }
+
+ protected void zoomIn() {
+ zoomIn(sScaleRate);
+ }
+
+ protected void zoomOut() {
+ zoomOut(sScaleRate);
+ }
+
+ protected void zoomIn(float rate) {
+ if (getScale() >= mMaxZoom) {
+ return; // Don't let the user zoom into the molecular level.
+ }
+ if (mBitmapDisplayed == null) {
+ return;
+ }
+ float width = getWidth();
+ float height = getHeight();
+
+ mSuppMatrix.postScale(rate, rate, width/2F, height/2F);
+ setImageMatrix(getImageViewMatrix());
+
+ onZoom();
+ }
+
+ protected void zoomOut(float rate) {
+ if (mBitmapDisplayed == null) {
+ return;
+ }
+
+ float width = getWidth();
+ float height = getHeight();
+
+ Matrix tmp = new Matrix(mSuppMatrix);
+ tmp.postScale(1F/sScaleRate, 1F/sScaleRate, width/2F, height/2F);
+ if (getScale(tmp) < 1F) {
+ mSuppMatrix.setScale(1F, 1F, width/2F, height/2F);
+ } else {
+ mSuppMatrix.postScale(1F/rate, 1F/rate, width/2F, height/2F);
+ }
+ setImageMatrix(getImageViewMatrix());
+ center(true, true, false);
+
+ onZoom();
+ }
+
+ protected void postTranslate(float dx, float dy) {
+ mSuppMatrix.postTranslate(dx, dy);
+ }
+
+ protected void panBy(float dx, float dy) {
+ postTranslate(dx, dy);
+ setImageMatrix(getImageViewMatrix());
+ }
+}
+
diff --git a/src/com/android/camera/MenuHelper.java b/src/com/android/camera/MenuHelper.java
new file mode 100644
index 0000000..7148cd6
--- /dev/null
+++ b/src/com/android/camera/MenuHelper.java
@@ -0,0 +1,720 @@
+/*
+ * Copyright (C) 2008 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;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.StatFs;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.util.Config;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.camera.ImageManager.IImage;
+
+public class MenuHelper {
+ static private final String TAG = "MenuHelper";
+
+ static public final int GENERIC_ITEM = 1;
+ static public final int IMAGE_SAVING_ITEM = 2;
+ static public final int VIDEO_SAVING_ITEM = 3;
+ static public final int IMAGE_MODE_ITEM = 4;
+ static public final int VIDEO_MODE_ITEM = 5;
+ static public final int MENU_ITEM_MAX = 5;
+
+ static public final int INCLUDE_ALL = 0xFFFFFFFF;
+ static public final int INCLUDE_VIEWPLAY_MENU = (1 << 0);
+ static public final int INCLUDE_SHARE_MENU = (1 << 1);
+ static public final int INCLUDE_SET_MENU = (1 << 2);
+ static public final int INCLUDE_CROP_MENU = (1 << 3);
+ static public final int INCLUDE_DELETE_MENU = (1 << 4);
+ static public final int INCLUDE_ROTATE_MENU = (1 << 5);
+ static public final int INCLUDE_DETAILS_MENU = (1 << 5);
+
+ static public final int MENU_SWITCH_CAMERA_MODE = 0;
+ static public final int MENU_CAPTURE_PICTURE = 1;
+ static public final int MENU_CAPTURE_VIDEO = 2;
+ static public final int MENU_IMAGE_SHARE = 10;
+ static public final int MENU_IMAGE_SET = 14;
+ static public final int MENU_IMAGE_SET_WALLPAPER = 15;
+ static public final int MENU_IMAGE_SET_CONTACT = 16;
+ static public final int MENU_IMAGE_SET_MYFAVE = 17;
+ static public final int MENU_IMAGE_CROP = 18;
+ static public final int MENU_IMAGE_ROTATE = 19;
+ static public final int MENU_IMAGE_ROTATE_LEFT = 20;
+ static public final int MENU_IMAGE_ROTATE_RIGHT = 21;
+ static public final int MENU_IMAGE_TOSS = 22;
+ static public final int MENU_VIDEO_PLAY = 23;
+ static public final int MENU_VIDEO_SHARE = 24;
+ static public final int MENU_VIDEO_TOSS = 27;
+
+ public static final int NO_STORAGE_ERROR = -1;
+ public static final int CANNOT_STAT_ERROR = -2;
+
+ /** Activity result code used to report crop results.
+ */
+ public static final int RESULT_COMMON_MENU_CROP = 490;
+
+ public interface MenuItemsResult {
+ public void gettingReadyToOpen(Menu menu, ImageManager.IImage image);
+ public void aboutToCall(MenuItem item, ImageManager.IImage image);
+ }
+
+ public interface MenuInvoker {
+ public void run(MenuCallback r);
+ }
+
+ public interface MenuCallback {
+ public void run(Uri uri, ImageManager.IImage image);
+ }
+
+ private static void closeSilently(Closeable target) {
+ try {
+ if (target != null) target.close();
+ } catch (Throwable t) {
+ // ignore all exceptions, that's what silently means
+ }
+ }
+
+ public static long getImageFileSize(ImageManager.IImage image) {
+ java.io.InputStream data = image.fullSizeImageData();
+ try {
+ return data.available();
+ } catch (java.io.IOException ex) {
+ return -1;
+ } finally {
+ closeSilently(data);
+ }
+ }
+
+ static MenuItemsResult addImageMenuItems(
+ Menu menu,
+ int inclusions,
+ final boolean isImage,
+ final Activity activity,
+ final Handler handler,
+ final Runnable onDelete,
+ final MenuInvoker onInvoke) {
+ final ArrayList<MenuItem> requiresWriteAccessItems = new ArrayList<MenuItem>();
+ final ArrayList<MenuItem> requiresNoDrmAccessItems = new ArrayList<MenuItem>();
+
+ if (isImage && ((inclusions & INCLUDE_ROTATE_MENU) != 0)) {
+ SubMenu rotateSubmenu = menu.addSubMenu(IMAGE_SAVING_ITEM, MENU_IMAGE_ROTATE,
+ 40, R.string.rotate).setIcon(android.R.drawable.ic_menu_rotate);
+ // Don't show the rotate submenu if the item at hand is read only
+ // since the items within the submenu won't be shown anyway. This is
+ // really a framework bug in that it shouldn't show the submenu if
+ // the submenu has no visible items.
+ requiresWriteAccessItems.add(rotateSubmenu.getItem());
+ if (rotateSubmenu != null) {
+ requiresWriteAccessItems.add(rotateSubmenu.add(0, MENU_IMAGE_ROTATE_LEFT, 50, R.string.rotate_left).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (image == null || image.isReadonly())
+ return;
+ image.rotateImageBy(-90);
+ }
+ });
+ return true;
+ }
+ }).setAlphabeticShortcut('l'));
+ requiresWriteAccessItems.add(rotateSubmenu.add(0, MENU_IMAGE_ROTATE_RIGHT, 60, R.string.rotate_right).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (image == null || image.isReadonly())
+ return;
+
+ image.rotateImageBy(90);
+ }
+ });
+ return true;
+ }
+ }).setAlphabeticShortcut('r'));
+ }
+ }
+
+ if (isImage && ((inclusions & INCLUDE_CROP_MENU) != 0)) {
+ MenuItem autoCrop = menu.add(IMAGE_SAVING_ITEM, MENU_IMAGE_CROP, 73,
+ R.string.camera_crop).setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (u == null)
+ return;
+
+ Intent cropIntent = new Intent();
+ cropIntent.setClass(activity, CropImage.class);
+ cropIntent.setData(u);
+ activity.startActivityForResult(cropIntent, RESULT_COMMON_MENU_CROP);
+ }
+ });
+ return true;
+ }
+ });
+ autoCrop.setIcon(android.R.drawable.ic_menu_crop);
+ requiresWriteAccessItems.add(autoCrop);
+ }
+
+ if (isImage && ((inclusions & INCLUDE_SET_MENU) != 0)) {
+ MenuItem setMenu = menu.add(IMAGE_SAVING_ITEM, MENU_IMAGE_SET, 75, R.string.camera_set);
+ setMenu.setIcon(android.R.drawable.ic_menu_set_as);
+
+ setMenu.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (u == null || image == null)
+ return;
+
+ if (Config.LOGV)
+ Log.v(TAG, "in callback u is " + u + "; mime type is " + image.getMimeType());
+ Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
+ intent.setDataAndType(u, image.getMimeType());
+ intent.putExtra("mimeType", image.getMimeType());
+ activity.startActivity(Intent.createChooser(intent, activity.getText(R.string.setImage)));
+ }
+ });
+ return true;
+ }
+ });
+ }
+
+ if ((inclusions & INCLUDE_SHARE_MENU) != 0) {
+ if (Config.LOGV)
+ Log.v(TAG, ">>>>> add share");
+ MenuItem item1 = menu.add(IMAGE_SAVING_ITEM, MENU_IMAGE_SHARE, 10,
+ R.string.camera_share).setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (image == null)
+ return;
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ String mimeType = image.getMimeType();
+ intent.setType(mimeType);
+ intent.putExtra(Intent.EXTRA_STREAM, u);
+ boolean isImage = ImageManager.isImageMimeType(mimeType);
+ try {
+ activity.startActivity(Intent.createChooser(intent,
+ activity.getText(
+ isImage ? R.string.sendImage : R.string.sendVideo)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(activity,
+ isImage ? R.string.no_way_to_share_image
+ : R.string.no_way_to_share_video,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ return true;
+ }
+ });
+ item1.setIcon(android.R.drawable.ic_menu_share);
+ MenuItem item = item1;
+ requiresNoDrmAccessItems.add(item);
+ }
+
+ if ((inclusions & INCLUDE_DELETE_MENU) != 0) {
+ MenuItem deleteItem = menu.add(IMAGE_SAVING_ITEM, MENU_IMAGE_TOSS, 70, R.string.camera_toss);
+ requiresWriteAccessItems.add(deleteItem);
+ deleteItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ deleteImageImpl(activity, onDelete, isImage);
+ return true;
+ }
+ })
+ .setAlphabeticShortcut('d')
+ .setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ if ((inclusions & INCLUDE_DETAILS_MENU) != 0) {
+ MenuItem detailsMenu = menu.add(0, 0, 80, R.string.details).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (image == null)
+ return;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+
+ final View d = View.inflate(activity, R.layout.detailsview, null);
+
+ ImageView imageView = (ImageView) d.findViewById(R.id.details_thumbnail_image);
+ imageView.setImageBitmap(image.miniThumbBitmap());
+
+ TextView textView = (TextView) d.findViewById(R.id.details_image_title);
+ textView.setText(image.getDisplayName());
+
+ long length = getImageFileSize(image);
+ String lengthString = lengthString = length < 0 ? ""
+ : android.text.format.Formatter.formatFileSize(activity, length);
+ ((TextView)d.findViewById(R.id.details_file_size_value))
+ .setText(lengthString);
+
+ int dimensionWidth = 0;
+ int dimensionHeight = 0;
+ if (isImage) {
+ dimensionWidth = image.getWidth();
+ dimensionHeight = image.getHeight();
+ d.findViewById(R.id.details_duration_row).setVisibility(View.GONE);
+ d.findViewById(R.id.details_frame_rate_row).setVisibility(View.GONE);
+ d.findViewById(R.id.details_bit_rate_row).setVisibility(View.GONE);
+ d.findViewById(R.id.details_format_row).setVisibility(View.GONE);
+ d.findViewById(R.id.details_codec_row).setVisibility(View.GONE);
+ } else {
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ try {
+ retriever.setMode(MediaMetadataRetriever.MODE_GET_METADATA_ONLY);
+ retriever.setDataSource(image.getDataPath());
+ try {
+ dimensionWidth = Integer.parseInt(
+ retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
+ dimensionHeight = Integer.parseInt(
+ retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
+ } catch (NumberFormatException e) {
+ dimensionWidth = 0;
+ dimensionHeight = 0;
+ }
+
+ try {
+ int durationMs = Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_DURATION));
+ String durationValue = formatDuration(
+ activity, durationMs);
+ ((TextView)d.findViewById(R.id.details_duration_value))
+ .setText(durationValue);
+ } catch (NumberFormatException e) {
+ d.findViewById(R.id.details_frame_rate_row)
+ .setVisibility(View.GONE);
+ }
+
+ try {
+ String frame_rate = String.format(
+ activity.getString(R.string.details_fps),
+ Integer.parseInt(
+ retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_FRAME_RATE)));
+ ((TextView)d.findViewById(R.id.details_frame_rate_value))
+ .setText(frame_rate);
+ } catch (NumberFormatException e) {
+ d.findViewById(R.id.details_frame_rate_row)
+ .setVisibility(View.GONE);
+ }
+
+ try {
+ long bitRate = Long.parseLong(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_BIT_RATE));
+ String bps;
+ if (bitRate < 1000000) {
+ bps = String.format(
+ activity.getString(R.string.details_kbps),
+ bitRate / 1000);
+ } else {
+ bps = String.format(
+ activity.getString(R.string.details_mbps),
+ ((double) bitRate) / 1000000.0);
+ }
+ ((TextView)d.findViewById(R.id.details_bit_rate_value))
+ .setText(bps);
+ } catch (NumberFormatException e) {
+ d.findViewById(R.id.details_bit_rate_row)
+ .setVisibility(View.GONE);
+ }
+
+ String format = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_FORMAT);
+ ((TextView)d.findViewById(R.id.details_format_value))
+ .setText(format);
+
+ String codec = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_CODEC);
+
+ if (codec == null) {
+ d.findViewById(R.id.details_codec_row).
+ setVisibility(View.GONE);
+ } else {
+ ((TextView)d.findViewById(R.id.details_codec_value))
+ .setText(codec);
+ }
+ } catch(RuntimeException ex) {
+ // Assume this is a corrupt video file.
+ } finally {
+ try {
+ retriever.release();
+ } catch (RuntimeException ex) {
+ // Ignore failures while cleaning up.
+ }
+ }
+ }
+
+ String dimensionsString = String.format(
+ activity.getString(R.string.details_dimension_x),
+ dimensionWidth, dimensionHeight);
+ ((TextView)d.findViewById(R.id.details_resolution_value))
+ .setText(dimensionsString);
+
+ String dateString = "";
+ long dateTaken = image.getDateTaken();
+ if (dateTaken != 0) {
+ java.util.Date date = new java.util.Date(image.getDateTaken());
+ java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat();
+ dateString = dateFormat.format(date);
+
+ ((TextView)d.findViewById(R.id.details_date_taken_value))
+ .setText(dateString);
+ } else {
+ d.findViewById(R.id.details_date_taken_row)
+ .setVisibility(View.GONE);
+ }
+
+ builder.setNeutralButton(R.string.details_ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ builder.setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(R.string.details_panel_title)
+ .setView(d)
+ .show();
+
+ }
+ });
+ return true;
+ }
+ });
+ detailsMenu.setIcon(R.drawable.ic_menu_view_details);
+ }
+
+ if ((!isImage) && ((inclusions & INCLUDE_VIEWPLAY_MENU) != 0)) {
+ menu.add(VIDEO_SAVING_ITEM, MENU_VIDEO_PLAY, 0, R.string.video_play)
+ .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri uri, IImage image) {
+ if (image != null) {
+ Intent intent = new Intent(Intent.ACTION_VIEW,
+ image.fullSizeImageUri());
+ activity.startActivity(intent);
+ }
+ }});
+ return true;
+ }
+ });
+ }
+
+
+ return new MenuItemsResult() {
+ public void gettingReadyToOpen(Menu menu, ImageManager.IImage image) {
+ // protect against null here. this isn't strictly speaking required
+ // but if a client app isn't handling sdcard removal properly it
+ // could happen
+ if (image == null) {
+ return;
+ }
+ boolean readOnly = image.isReadonly();
+ boolean isDrm = image.isDrm();
+ if (Config.LOGV)
+ Log.v(TAG, "readOnly: " + readOnly + "; drm: " + isDrm);
+ for (MenuItem item: requiresWriteAccessItems) {
+ if (Config.LOGV)
+ Log.v(TAG, "item is " + item.toString());
+ item.setVisible(!readOnly);
+ item.setEnabled(!readOnly);
+ }
+ for (MenuItem item: requiresNoDrmAccessItems) {
+ if (Config.LOGV)
+ Log.v(TAG, "item is " + item.toString());
+ item.setVisible(!isDrm);
+ item.setEnabled(!isDrm);
+ }
+ }
+ public void aboutToCall(MenuItem menu, ImageManager.IImage image) {
+ }
+ };
+ }
+
+ static void deletePhoto(Activity activity, Runnable onDelete) {
+ deleteImageImpl(activity, onDelete, true);
+ }
+
+ static void deleteImage(Activity activity, Runnable onDelete, IImage image) {
+ if (image != null) {
+ deleteImageImpl(activity, onDelete, ImageManager.isImage(image));
+ }
+ }
+
+ private static void deleteImageImpl(Activity activity, final Runnable onDelete, boolean isPhoto) {
+ boolean confirm = android.preference.PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("pref_gallery_confirm_delete_key", true);
+ if (!confirm) {
+ if (onDelete != null)
+ onDelete.run();
+ } else {
+ displayDeleteDialog(activity, onDelete, isPhoto);
+ }
+ }
+
+ public static void displayDeleteDialog(Activity activity,
+ final Runnable onDelete, boolean isPhoto) {
+ android.app.AlertDialog.Builder b = new android.app.AlertDialog.Builder(activity);
+ b.setIcon(android.R.drawable.ic_dialog_alert);
+ b.setTitle(R.string.confirm_delete_title);
+ b.setMessage(isPhoto? R.string.confirm_delete_message
+ : R.string.confirm_delete_video_message);
+ b.setPositiveButton(android.R.string.ok, new android.content.DialogInterface.OnClickListener() {
+ public void onClick(android.content.DialogInterface v, int x) {
+ if (onDelete != null)
+ onDelete.run();
+ }
+ });
+ b.setNegativeButton(android.R.string.cancel, new android.content.DialogInterface.OnClickListener() {
+ public void onClick(android.content.DialogInterface v, int x) {
+
+ }
+ });
+ b.create().show();
+ }
+
+ static void addSwitchModeMenuItem(Menu menu, final Activity activity,
+ final boolean switchToVideo) {
+ int group = switchToVideo ? MenuHelper.IMAGE_MODE_ITEM : MenuHelper.VIDEO_MODE_ITEM;
+ int labelId = switchToVideo ? R.string.switch_to_video_lable
+ : R.string.switch_to_camera_lable;
+ int iconId = switchToVideo ? R.drawable.ic_menu_camera_video_view
+ : android.R.drawable.ic_menu_camera;
+ MenuItem item = menu.add(group, MENU_SWITCH_CAMERA_MODE, 0,
+ labelId).setOnMenuItemClickListener(
+ new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ String action = switchToVideo ? MediaStore.INTENT_ACTION_VIDEO_CAMERA
+ : MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA;
+ Intent intent = new Intent(action);
+ intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ activity.finish();
+ activity.startActivity(intent);
+ return true;
+ }
+ });
+ item.setIcon(iconId);
+ }
+
+ static void gotoStillImageCapture(Activity activity) {
+ Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ try {
+ activity.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Could not start still image capture activity", e);
+ }
+ }
+
+ static void gotoCameraImageGallery(Activity activity) {
+ gotoGallery(activity, R.string.gallery_camera_bucket_name, ImageManager.INCLUDE_IMAGES);
+ }
+
+ static void gotoCameraVideoGallery(Activity activity) {
+ gotoGallery(activity, R.string.gallery_camera_videos_bucket_name,
+ ImageManager.INCLUDE_VIDEOS);
+ }
+
+ static private void gotoGallery(Activity activity, int windowTitleId, int mediaTypes) {
+ Uri target = Images.Media.INTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("bucketId",
+ ImageManager.CAMERA_IMAGE_BUCKET_ID).build();
+ Intent intent = new Intent(Intent.ACTION_VIEW, target);
+ intent.putExtra("windowTitle", activity.getString(windowTitleId));
+ intent.putExtra("mediaTypes", mediaTypes);
+ // Request unspecified so that we match the current camera orientation rather than
+ // matching the "flip orientation" preference.
+ // Disabled because people don't care for it. Also it's
+ // not as compelling now that we have implemented have quick orientation flipping.
+ // intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
+ // android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ try {
+ activity.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Could not start gallery activity", e);
+ }
+ }
+
+ static void addCaptureMenuItems(Menu menu, final Activity activity) {
+
+ menu.add(0, MENU_CAPTURE_PICTURE, 1, R.string.capture_picture)
+ .setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
+ try {
+ activity.startActivity(intent);
+ } catch (android.content.ActivityNotFoundException e) {
+ // Ignore exception
+ }
+ return true;
+ }
+ })
+ .setIcon(android.R.drawable.ic_menu_camera);
+
+ menu.add(0, MENU_CAPTURE_VIDEO, 2, R.string.capture_video)
+ .setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+ try {
+ activity.startActivity(intent);
+ } catch (android.content.ActivityNotFoundException e) {
+ // Ignore exception
+ }
+ return true;
+ }
+ })
+ .setIcon(R.drawable.ic_menu_camera_video_view);
+ }
+ static MenuItem addFlipOrientation(Menu menu, final Activity activity, final SharedPreferences prefs) {
+ // position 41 after rotate
+ // D
+ return menu
+ .add(Menu.CATEGORY_SECONDARY, 304, 41, R.string.flip_orientation)
+ .setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // Check what our actual orientation is
+ int current = activity.getResources().getConfiguration().orientation;
+ int newOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ if (current == Configuration.ORIENTATION_LANDSCAPE) {
+ newOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt("nuorientation", newOrientation);
+ editor.commit();
+ requestOrientation(activity, prefs, true);
+ return true;
+ }
+ })
+ .setIcon(android.R.drawable.ic_menu_always_landscape_portrait);
+ }
+
+ static void requestOrientation(Activity activity, SharedPreferences prefs) {
+ requestOrientation(activity, prefs, false);
+ }
+
+ static private void requestOrientation(Activity activity, SharedPreferences prefs,
+ boolean ignoreIntentExtra) {
+ int req = prefs.getInt("nuorientation",
+ android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ // A little trick: use USER instead of UNSPECIFIED, so we ignore the
+ // orientation set by the activity below. It may have forced a landscape
+ // orientation, which the user has now cleared here.
+ if (req == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
+ req = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
+ }
+ if (! ignoreIntentExtra) {
+ Intent intent = activity.getIntent();
+ req = intent.getIntExtra(MediaStore.EXTRA_SCREEN_ORIENTATION, req);
+ }
+ activity.setRequestedOrientation(req);
+ }
+
+ static void setFlipOrientationEnabled(Activity activity, MenuItem flipItem) {
+ int keyboard = activity.getResources().getConfiguration().hardKeyboardHidden;
+ flipItem.setEnabled(keyboard != android.content.res.Configuration.HARDKEYBOARDHIDDEN_NO);
+ }
+
+ public static String formatDuration(final Activity activity, int durationMs) {
+ int duration = durationMs / 1000;
+ int h = duration / 3600;
+ int m = (duration - h * 3600) / 60;
+ int s = duration - (h * 3600 + m * 60);
+ String durationValue;
+ if (h == 0) {
+ durationValue = String.format(
+ activity.getString(R.string.details_ms), m, s);
+ } else {
+ durationValue = String.format(
+ activity.getString(R.string.details_hms), h, m, s);
+ }
+ return durationValue;
+ }
+
+ public static void showStorageToast(Activity activity) {
+ showStorageToast(activity, calculatePicturesRemaining());
+ }
+
+ public static void showStorageToast(Activity activity, int remaining) {
+ String noStorageText = null;
+
+ if (remaining == MenuHelper.NO_STORAGE_ERROR) {
+ String state = Environment.getExternalStorageState();
+ if (state == Environment.MEDIA_CHECKING) {
+ noStorageText = activity.getString(R.string.preparing_sd);
+ } else {
+ noStorageText = activity.getString(R.string.no_storage);
+ }
+ } else if (remaining < 1) {
+ noStorageText = activity.getString(R.string.not_enough_space);
+ }
+
+ if (noStorageText != null) {
+ Toast.makeText(activity, noStorageText, 5000).show();
+ }
+ }
+
+ public static int calculatePicturesRemaining() {
+ try {
+ if (!ImageManager.hasStorage()) {
+ return NO_STORAGE_ERROR;
+ } else {
+ String storageDirectory = Environment.getExternalStorageDirectory().toString();
+ StatFs stat = new StatFs(storageDirectory);
+ float remaining = ((float)stat.getAvailableBlocks() * (float)stat.getBlockSize()) / 400000F;
+ return (int)remaining;
+ }
+ } catch (Exception ex) {
+ // if we can't stat the filesystem then we don't know how many
+ // pictures are remaining. it might be zero but just leave it
+ // blank since we really don't know.
+ return CANNOT_STAT_ERROR;
+ }
+ }
+}
+
diff --git a/src/com/android/camera/MovieView.java b/src/com/android/camera/MovieView.java
new file mode 100644
index 0000000..bf0e6ca
--- /dev/null
+++ b/src/com/android/camera/MovieView.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2007 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;
+
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentValues;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Video;
+import android.view.View;
+import android.widget.MediaController;
+import android.widget.VideoView;
+
+public class MovieView extends Activity implements MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener
+{
+ private static final String TAG = "MovieView";
+ // Copied from MediaPlaybackService in the Music Player app. Should be public, but isn't.
+ private static final String SERVICECMD = "com.android.music.musicservicecommand";
+ private static final String CMDNAME = "command";
+ private static final String CMDPAUSE = "pause";
+
+ private VideoView mVideoView;
+ private View mProgressView;
+ private boolean mFinishOnCompletion;
+ private Uri mUri;
+
+ // State maintained for proper onPause/OnResume behaviour.
+ private int mPositionWhenPaused = -1;
+ private boolean mWasPlayingWhenPaused = false;
+
+ public MovieView()
+ {
+ }
+
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.movie_view);
+
+ mVideoView = (VideoView) findViewById(R.id.surface_view);
+ mProgressView = findViewById(R.id.progress_indicator);
+ Intent intent = getIntent();
+ if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) {
+ int orientation = intent.getIntExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ if (orientation != getRequestedOrientation()) {
+ setRequestedOrientation(orientation);
+ }
+ }
+ mFinishOnCompletion = intent.getBooleanExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, true);
+ mUri = intent.getData();
+
+ // For streams that we expect to be slow to start up, show a
+ // progress spinner until playback starts.
+ String scheme = mUri.getScheme();
+ if ("http".equalsIgnoreCase(scheme) ||
+ "rtsp".equalsIgnoreCase(scheme)) {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ } else {
+ mProgressView.setVisibility(View.GONE);
+ }
+
+ mVideoView.setOnErrorListener(this);
+ mVideoView.setOnCompletionListener(this);
+ mVideoView.setVideoURI(mUri);
+ mVideoView.setMediaController(new MediaController(this));
+ mVideoView.requestFocus(); // make the video view handle keys for seeking and pausing
+
+ Intent i = new Intent(SERVICECMD);
+ i.putExtra(CMDNAME, CMDPAUSE);
+ sendBroadcast(i);
+ {
+ final Integer bookmark = getBookmark();
+ if (bookmark != null) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.resume_playing_title);
+ builder.setMessage(String.format(
+ getString(R.string.resume_playing_message),
+ MenuHelper.formatDuration(this, bookmark)));
+ builder.setOnCancelListener(new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }});
+ builder.setPositiveButton(R.string.resume_playing_resume,
+ new OnClickListener(){
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.seekTo(bookmark);
+ mVideoView.start();
+ }});
+ builder.setNegativeButton(R.string.resume_playing_restart, new OnClickListener(){
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.start();
+ }});
+ builder.show();
+ } else {
+ mVideoView.start();
+ }
+ }
+ }
+
+ private static boolean uriSupportsBookmarks(Uri uri) {
+ String scheme = uri.getScheme();
+ String authority = uri.getAuthority();
+ return ("content".equalsIgnoreCase(scheme)
+ && MediaStore.AUTHORITY.equalsIgnoreCase(authority));
+ }
+
+ private Integer getBookmark() {
+ if (!uriSupportsBookmarks(mUri)) {
+ return null;
+ }
+
+ String[] projection = new String[]{Video.VideoColumns.DURATION,
+ Video.VideoColumns.BOOKMARK};
+ try {
+ Cursor cursor = getContentResolver().query(mUri, projection, null, null, null);
+ if (cursor != null) {
+ try {
+ if ( cursor.moveToFirst() ) {
+ int duration = getCursorInteger(cursor, 0);
+ int bookmark = getCursorInteger(cursor, 1);
+ final int ONE_MINUTE = 60 * 1000;
+ final int TWO_MINUTES = 2 * ONE_MINUTE;
+ final int FIVE_MINUTES = 5 * ONE_MINUTE;
+ if ((bookmark < TWO_MINUTES)
+ || (duration < FIVE_MINUTES)
+ || (bookmark > (duration - ONE_MINUTE))) {
+ return null;
+ }
+
+ return new Integer(bookmark);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ } catch (SQLiteException e) {
+ // ignore
+ }
+
+ return null;
+ }
+
+ private int getCursorInteger(Cursor cursor, int index) {
+ try {
+ return cursor.getInt(index);
+ } catch (SQLiteException e) {
+ return 0;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+
+ }
+
+ private void setBookmark(int bookmark) {
+ if (!uriSupportsBookmarks(mUri)) {
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Video.VideoColumns.BOOKMARK, Integer.toString(bookmark));
+ try {
+ getContentResolver().update(mUri, values, null, null);
+ } catch (SecurityException ex) {
+ // Ignore, can happen if we try to set the bookmark on a read-only resource
+ // such as a video attached to GMail.
+ } catch (SQLiteException e) {
+ // ignore. can happen if the content doesn't support a bookmark column.
+ } catch (UnsupportedOperationException e) {
+ // ignore. can happen if the external volume is already detached.
+ }
+ }
+
+ @Override
+ public void onPause() {
+ mHandler.removeCallbacksAndMessages(null);
+ setBookmark(mVideoView.getCurrentPosition());
+
+ mPositionWhenPaused = mVideoView.getCurrentPosition();
+ mWasPlayingWhenPaused = mVideoView.isPlaying();
+ mVideoView.stopPlayback();
+
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ if (mPositionWhenPaused >= 0) {
+ mVideoView.setVideoURI(mUri);
+ mVideoView.seekTo(mPositionWhenPaused);
+ if (mWasPlayingWhenPaused) {
+ mVideoView.start();
+ }
+ }
+
+ super.onResume();
+ }
+
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ }
+ };
+
+ Runnable mPlayingChecker = new Runnable() {
+ public void run() {
+ if (mVideoView.isPlaying()) {
+ mProgressView.setVisibility(View.GONE);
+ } else {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ }
+ }
+ };
+
+ public boolean onError(MediaPlayer player, int arg1, int arg2) {
+ mHandler.removeCallbacksAndMessages(null);
+ mProgressView.setVisibility(View.GONE);
+ return false;
+ }
+
+ public void onCompletion(MediaPlayer mp) {
+ if (mFinishOnCompletion) {
+ finish();
+ }
+ }
+}
diff --git a/src/com/android/camera/OnScreenHint.java b/src/com/android/camera/OnScreenHint.java
new file mode 100644
index 0000000..96190a0
--- /dev/null
+++ b/src/com/android/camera/OnScreenHint.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2009 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;
+
+import android.app.INotificationManager;
+import android.app.ITransientNotification;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+/**
+ * A on-screen hint is a view containing a little message for the user and will
+ * be shown on the screen continuously. This class helps you create and show
+ * those.
+ *
+ * <p>
+ * When the view is shown to the user, appears as a floating view over the
+ * application.
+ * <p>
+ * The easiest way to use this class is to call one of the static methods that
+ * constructs everything you need and returns a new OnScreenHint object.
+ */
+public class OnScreenHint {
+ static final String TAG = "OnScreenHint";
+ static final boolean localLOGV = false;
+
+ final Context mContext;
+ int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+ int mX, mY;
+ float mHorizontalMargin;
+ float mVerticalMargin;
+ View mView;
+ View mNextView;
+
+ private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+ private WindowManager mWM;
+ private final Handler mHandler = new Handler();
+
+ /**
+ * Construct an empty OnScreenHint object. You must call {@link #setView} before you
+ * can call {@link #show}.
+ *
+ * @param context The context to use. Usually your {@link android.app.Application}
+ * or {@link android.app.Activity} object.
+ */
+ public OnScreenHint(Context context) {
+ mContext = context;
+ mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ mY = context.getResources().getDimensionPixelSize(R.dimen.hint_y_offset);
+
+ mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+ mParams.format = PixelFormat.TRANSLUCENT;
+ mParams.windowAnimations = R.style.Animation_OnScreenHint;
+ mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+ mParams.setTitle("OnScreenHint");
+ }
+
+ /**
+ * Show the view on the screen.
+ */
+ public void show() {
+ if (mNextView == null) {
+ throw new RuntimeException("setView must have been called");
+ }
+ if (localLOGV) Log.v(TAG, "SHOW: " + this);
+ mHandler.post(mShow);
+ }
+
+ /**
+ * Close the view if it's showing.
+ */
+ public void cancel() {
+ if (localLOGV) Log.v(TAG, "HIDE: " + this);
+ mHandler.post(mHide);
+ }
+
+ /**
+ * Set the view to show.
+ * @see #getView
+ */
+ public void setView(View view) {
+ mNextView = view;
+ }
+
+ /**
+ * Return the view.
+ * @see #setView
+ */
+ public View getView() {
+ return mNextView;
+ }
+
+ /**
+ * Set the margins of the view.
+ *
+ * @param horizontalMargin The horizontal margin, in percentage of the
+ * container width, between the container's edges and the
+ * notification
+ * @param verticalMargin The vertical margin, in percentage of the
+ * container height, between the container's edges and the
+ * notification
+ */
+ public void setMargin(float horizontalMargin, float verticalMargin) {
+ mHorizontalMargin = horizontalMargin;
+ mVerticalMargin = verticalMargin;
+ }
+
+ /**
+ * Return the horizontal margin.
+ */
+ public float getHorizontalMargin() {
+ return mHorizontalMargin;
+ }
+
+ /**
+ * Return the vertical margin.
+ */
+ public float getVerticalMargin() {
+ return mVerticalMargin;
+ }
+
+ /**
+ * Set the location at which the notification should appear on the screen.
+ * @see android.view.Gravity
+ * @see #getGravity
+ */
+ public void setGravity(int gravity, int xOffset, int yOffset) {
+ mGravity = gravity;
+ mX = xOffset;
+ mY = yOffset;
+ }
+
+ /**
+ * Get the location at which the notification should appear on the screen.
+ * @see android.view.Gravity
+ * @see #getGravity
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * Return the X offset in pixels to apply to the gravity's location.
+ */
+ public int getXOffset() {
+ return mX;
+ }
+
+ /**
+ * Return the Y offset in pixels to apply to the gravity's location.
+ */
+ public int getYOffset() {
+ return mY;
+ }
+
+ /**
+ * Make a standard hint that just contains a text view.
+ *
+ * @param context The context to use. Usually your {@link android.app.Application}
+ * or {@link android.app.Activity} object.
+ * @param text The text to show. Can be formatted text.
+ *
+ */
+ public static OnScreenHint makeText(Context context, CharSequence text) {
+ OnScreenHint result = new OnScreenHint(context);
+
+ LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View v = inflate.inflate(R.layout.on_screen_hint, null);
+ TextView tv = (TextView)v.findViewById(R.id.message);
+ tv.setText(text);
+
+ result.mNextView = v;
+
+ return result;
+ }
+
+ /**
+ * Make a standard hint that just contains a text view with the text from a resource.
+ *
+ * @param context The context to use. Usually your {@link android.app.Application}
+ * or {@link android.app.Activity} object.
+ * @param resId The resource id of the string resource to use. Can be formatted text.
+ *
+ * @throws Resources.NotFoundException if the resource can't be found.
+ */
+ public static OnScreenHint makeText(Context context, int resId)
+ throws Resources.NotFoundException {
+ return makeText(context, context.getResources().getText(resId));
+ }
+
+ /**
+ * Update the text in a OnScreenHint that was previously created using one of the makeText() methods.
+ * @param resId The new text for the OnScreenHint.
+ */
+ public void setText(int resId) {
+ setText(mContext.getText(resId));
+ }
+
+ /**
+ * Update the text in a OnScreenHint that was previously created using one of the makeText() methods.
+ * @param s The new text for the OnScreenHint.
+ */
+ public void setText(CharSequence s) {
+ if (mNextView == null) {
+ throw new RuntimeException("This OnScreenHint was not created with OnScreenHint.makeText()");
+ }
+ TextView tv = (TextView) mNextView.findViewById(R.id.message);
+ if (tv == null) {
+ throw new RuntimeException("This OnScreenHint was not created with OnScreenHint.makeText()");
+ }
+ tv.setText(s);
+ }
+
+ private synchronized void handleShow() {
+ if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ + " mNextView=" + mNextView);
+ if (mView != mNextView) {
+ // remove the old view if necessary
+ handleHide();
+ mView = mNextView;
+ final int gravity = mGravity;
+ mParams.gravity = gravity;
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
+ mParams.horizontalWeight = 1.0f;
+ }
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
+ mParams.verticalWeight = 1.0f;
+ }
+ mParams.x = mX;
+ mParams.y = mY;
+ mParams.verticalMargin = mVerticalMargin;
+ mParams.horizontalMargin = mHorizontalMargin;
+ if (mView.getParent() != null) {
+ if (localLOGV) Log.v(
+ TAG, "REMOVE! " + mView + " in " + this);
+ mWM.removeView(mView);
+ }
+ if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
+ mWM.addView(mView, mParams);
+ }
+ }
+
+ private synchronized void handleHide() {
+ if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
+ if (mView != null) {
+ // note: checking parent() just to make sure the view has
+ // been added... i have seen cases where we get here when
+ // the view isn't yet added, so let's try not to crash.
+ if (mView.getParent() != null) {
+ if (localLOGV) Log.v(
+ TAG, "REMOVE! " + mView + " in " + this);
+ mWM.removeView(mView);
+ }
+ mView = null;
+ }
+ }
+
+ private Runnable mShow = new Runnable() {
+ public void run() {
+ handleShow();
+ }
+ };
+
+ private Runnable mHide = new Runnable() {
+ public void run() {
+ handleHide();
+ }
+ };
+}
+
diff --git a/src/com/android/camera/PhotoGadgetBind.java b/src/com/android/camera/PhotoGadgetBind.java
new file mode 100644
index 0000000..fff19de
--- /dev/null
+++ b/src/com/android/camera/PhotoGadgetBind.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2009 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;
+
+import com.android.camera.PhotoGadgetProvider.PhotoDatabaseHelper;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.gadget.GadgetManager;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import java.util.ArrayList;
+
+public class PhotoGadgetBind extends Activity {
+ static final String TAG = "PhotoGadgetBind";
+
+ static final String EXTRA_GADGET_BITMAPS = "com.android.camera.gadgetbitmaps";
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ finish();
+
+ // The caller has requested that we bind a given bitmap to a specific
+ // gadgetId, which probably is happening during a Launcher upgrade. This
+ // is dangerous because the caller could set bitmaps on gadgetIds they
+ // don't own, so we guard this call at the manifest level by requiring
+ // the BIND_GADGET permission.
+
+ final Intent intent = getIntent();
+ final Bundle extras = intent.getExtras();
+
+ final int[] gadgetIds = extras.getIntArray(GadgetManager.EXTRA_GADGET_IDS);
+ final ArrayList<Bitmap> bitmaps = extras.getParcelableArrayList(EXTRA_GADGET_BITMAPS);
+
+ if (gadgetIds == null || bitmaps == null ||
+ gadgetIds.length != bitmaps.size()) {
+ Log.e(TAG, "Problem parsing photo gadget bind request");
+ return;
+ }
+
+ GadgetManager gadgetManager = GadgetManager.getInstance(this);
+ PhotoDatabaseHelper helper = new PhotoDatabaseHelper(this);
+ for (int i = 0; i < gadgetIds.length; i++) {
+ // Store the cropped photo in our database
+ int gadgetId = gadgetIds[i];
+ helper.setPhoto(gadgetId, bitmaps.get(i));
+
+ // Push newly updated gadget to surface
+ RemoteViews views = PhotoGadgetProvider.buildUpdate(this, gadgetId, helper);
+ gadgetManager.updateGadget(new int[] { gadgetId }, views);
+ }
+ helper.close();
+
+ }
+}
diff --git a/src/com/android/camera/PhotoGadgetConfigure.java b/src/com/android/camera/PhotoGadgetConfigure.java
new file mode 100644
index 0000000..a94b5a3
--- /dev/null
+++ b/src/com/android/camera/PhotoGadgetConfigure.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2009 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;
+
+import com.android.camera.PhotoGadgetProvider.PhotoDatabaseHelper;
+
+import android.app.Activity;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.sqlite.SQLiteDatabase;
+import android.gadget.GadgetManager;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class PhotoGadgetConfigure extends Activity {
+ static final private String TAG = "PhotoGadgetConfigure";
+
+ static final int REQUEST_GET_PHOTO = 2;
+
+ int gadgetId = -1;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Someone is requesting that we configure the given gadgetId, which means
+ // we prompt the user to pick and crop a photo.
+
+ gadgetId = getIntent().getIntExtra(GadgetManager.EXTRA_GADGET_ID, -1);
+ if (gadgetId == -1) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+
+ // TODO: get these values from constants somewhere
+ // TODO: Adjust the PhotoFrame's image size to avoid on the fly scaling
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+ intent.setType("image/*");
+ intent.putExtra("crop", "true");
+ intent.putExtra("aspectX", 1);
+ intent.putExtra("aspectY", 1);
+ intent.putExtra("outputX", 192);
+ intent.putExtra("outputY", 192);
+ intent.putExtra("noFaceDetection", true);
+ intent.putExtra("return-data", true);
+
+ startActivityForResult(intent, REQUEST_GET_PHOTO);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == RESULT_OK && gadgetId != -1) {
+ // Store the cropped photo in our database
+ Bitmap bitmap = (Bitmap) data.getParcelableExtra("data");
+
+ PhotoDatabaseHelper helper = new PhotoDatabaseHelper(this);
+ if (helper.setPhoto(gadgetId, bitmap)) {
+ resultCode = Activity.RESULT_OK;
+
+ // Push newly updated gadget to surface
+ RemoteViews views = PhotoGadgetProvider.buildUpdate(this, gadgetId, helper);
+ GadgetManager gadgetManager = GadgetManager.getInstance(this);
+ gadgetManager.updateGadget(new int[] { gadgetId }, views);
+ }
+ helper.close();
+ } else {
+ resultCode = Activity.RESULT_CANCELED;
+ }
+
+ // Make sure we pass back the original gadgetId
+ Intent resultValue = new Intent();
+ resultValue.putExtra(GadgetManager.EXTRA_GADGET_ID, gadgetId);
+ setResult(resultCode, resultValue);
+ finish();
+ }
+
+}
diff --git a/src/com/android/camera/PhotoGadgetProvider.java b/src/com/android/camera/PhotoGadgetProvider.java
new file mode 100644
index 0000000..b03217d
--- /dev/null
+++ b/src/com/android/camera/PhotoGadgetProvider.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2009 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;
+
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.gadget.GadgetManager;
+import android.gadget.GadgetProvider;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.Settings;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Instances;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.Config;
+import android.util.Log;
+import android.util.Xml;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Simple gadget to show a user-selected picture.
+ */
+public class PhotoGadgetProvider extends GadgetProvider {
+ static final String TAG = "PhotoGadgetProvider";
+ static final boolean LOGD = Config.LOGD || true;
+
+ @Override
+ public void onUpdate(Context context, GadgetManager gadgetManager, int[] gadgetIds) {
+ // Update each requested gadgetId with its unique photo
+ PhotoDatabaseHelper helper = new PhotoDatabaseHelper(context);
+ for (int gadgetId : gadgetIds) {
+ int[] specificGadget = new int[] { gadgetId };
+ RemoteViews views = buildUpdate(context, gadgetId, helper);
+ if (LOGD) Log.d(TAG, "sending out views="+views+" for id="+gadgetId);
+ gadgetManager.updateGadget(specificGadget, views);
+ }
+ helper.close();
+ }
+
+ @Override
+ public void onDeleted(Context context, int[] gadgetIds) {
+ // Clean deleted photos out of our database
+ PhotoDatabaseHelper helper = new PhotoDatabaseHelper(context);
+ for (int gadgetId : gadgetIds) {
+ helper.deletePhoto(gadgetId);
+ }
+ helper.close();
+ }
+
+ /**
+ * Load photo for given gadget and build {@link RemoteViews} for it.
+ */
+ static RemoteViews buildUpdate(Context context, int gadgetId, PhotoDatabaseHelper helper) {
+ RemoteViews views = null;
+ Bitmap bitmap = helper.getPhoto(gadgetId);
+ if (bitmap != null) {
+ views = new RemoteViews(context.getPackageName(), R.layout.photo_frame);
+ views.setImageViewBitmap(R.id.photo, bitmap);
+ }
+ return views;
+ }
+
+ static class PhotoDatabaseHelper extends SQLiteOpenHelper {
+ private final Context mContext;
+
+ private static final String DATABASE_NAME = "launcher.db";
+
+ private static final int DATABASE_VERSION = 1;
+
+ static final String TABLE_PHOTOS = "photos";
+ static final String FIELD_GADGET_ID = "gadgetId";
+ static final String FIELD_PHOTO_BLOB = "photoBlob";
+
+ PhotoDatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ mContext = context;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_PHOTOS + " (" +
+ FIELD_GADGET_ID + " INTEGER PRIMARY KEY," +
+ FIELD_PHOTO_BLOB + " BLOB" +
+ ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ int version = oldVersion;
+
+ if (version != DATABASE_VERSION) {
+ Log.w(TAG, "Destroying all old data.");
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_PHOTOS);
+ onCreate(db);
+ }
+ }
+
+ /**
+ * Store the given bitmap in this database for the given gadgetId.
+ */
+ public boolean setPhoto(int gadgetId, Bitmap bitmap) {
+ boolean success = false;
+ try {
+ // Try go guesstimate how much space the icon will take when serialized
+ // to avoid unnecessary allocations/copies during the write.
+ int size = bitmap.getWidth() * bitmap.getHeight() * 4;
+ ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.flush();
+ out.close();
+
+ ContentValues values = new ContentValues();
+ values.put(PhotoDatabaseHelper.FIELD_GADGET_ID, gadgetId);
+ values.put(PhotoDatabaseHelper.FIELD_PHOTO_BLOB, out.toByteArray());
+
+ SQLiteDatabase db = getWritableDatabase();
+ db.insertOrThrow(PhotoDatabaseHelper.TABLE_PHOTOS, null, values);
+
+ success = true;
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Could not open database", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not serialize photo", e);
+ }
+ if (LOGD) Log.d(TAG, "setPhoto success="+success);
+ return success;
+ }
+
+ static final String[] PHOTOS_PROJECTION = {
+ FIELD_PHOTO_BLOB,
+ };
+
+ static final int INDEX_PHOTO_BLOB = 0;
+
+ /**
+ * Inflate and return a bitmap for the given gadgetId.
+ */
+ public Bitmap getPhoto(int gadgetId) {
+ Cursor c = null;
+ Bitmap bitmap = null;
+ try {
+ SQLiteDatabase db = getReadableDatabase();
+ String selection = String.format("%s=%d", FIELD_GADGET_ID, gadgetId);
+ c = db.query(TABLE_PHOTOS, PHOTOS_PROJECTION, selection, null,
+ null, null, null, null);
+
+ if (c != null && LOGD) Log.d(TAG, "getPhoto query count="+c.getCount());
+
+ if (c != null && c.moveToFirst()) {
+ byte[] data = c.getBlob(INDEX_PHOTO_BLOB);
+ if (data != null) {
+ bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+ }
+ }
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Could not load photo from database", e);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return bitmap;
+ }
+
+ /**
+ * Remove any bitmap associated with the given gadgetId.
+ */
+ public void deletePhoto(int gadgetId) {
+ try {
+ SQLiteDatabase db = getWritableDatabase();
+ String whereClause = String.format("%s=%d", FIELD_GADGET_ID, gadgetId);
+ db.delete(TABLE_PHOTOS, whereClause, null);
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Could not delete photo from database", e);
+ }
+ }
+ }
+
+}
+
diff --git a/src/com/android/camera/PickWallpaper.java b/src/com/android/camera/PickWallpaper.java
new file mode 100644
index 0000000..e1fe784
--- /dev/null
+++ b/src/com/android/camera/PickWallpaper.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2007 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;
+
+/**
+ * Wallpaper picker for the camera application. This just redirects to the standard pick action.
+ */
+public class PickWallpaper extends Wallpaper {
+}
diff --git a/src/com/android/camera/SelectedImageGetter.java b/src/com/android/camera/SelectedImageGetter.java
new file mode 100644
index 0000000..9e8fb96
--- /dev/null
+++ b/src/com/android/camera/SelectedImageGetter.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.net.Uri;
+
+interface SelectedImageGetter {
+ ImageManager.IImage getCurrentImage();
+ Uri getCurrentImageUri();
+}
+
diff --git a/src/com/android/camera/ShutterButton.java b/src/com/android/camera/ShutterButton.java
new file mode 100644
index 0000000..bd8d042
--- /dev/null
+++ b/src/com/android/camera/ShutterButton.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2008 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;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A button designed to be used for the on-screen shutter button.
+ * It's currently an ImageView that can call a delegate when the pressed state changes.
+ */
+public class ShutterButton extends ImageView {
+ /**
+ * Interface definition for a callback to be invoked when a ModeButton's pressed state changes.
+ */
+ public interface OnShutterButtonListener {
+ /**
+ * Called when a ShutterButton has been pressed.
+ *
+ * @param b The ShutterButton that was pressed.
+ */
+ void onShutterButtonFocus(ShutterButton b, boolean pressed);
+ void onShutterButtonClick(ShutterButton b);
+ }
+
+ private OnShutterButtonListener mListener;
+ private boolean mOldPressed;
+
+ public ShutterButton(Context context) {
+ super(context);
+ }
+
+ public ShutterButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ShutterButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setOnShutterButtonListener(OnShutterButtonListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Hook into the drawable state changing to get changes to isPressed -- the
+ * onPressed listener doesn't always get called when the pressed state changes.
+ */
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ final boolean pressed = isPressed();
+ if (pressed != mOldPressed) {
+ if (!pressed) {
+ // When pressing the physical camera button the sequence of events is:
+ // focus pressed, optional camera pressed, focus released.
+ // We want to emulate this sequence of events with the shutter button.
+ // When clicking using a trackball button, the view system changes
+ // the the drawable state before posting click notification, so the
+ // sequence of events is:
+ // pressed(true), optional click, pressed(false)
+ // When clicking using touch events, the view system changes the
+ // drawable state after posting click notification, so the sequence of
+ // events is:
+ // pressed(true), pressed(false), optional click
+ // Since we're emulating the physical camera button, we want to have the
+ // same order of events. So we want the optional click callback to be delivered
+ // before the pressed(false) callback.
+ //
+ // To do this, we delay the posting of the pressed(false) event slightly by
+ // pushing it on the event queue. This moves it after the optional click
+ // notification, so our client always sees events in this sequence:
+ // pressed(true), optional click, pressed(false)
+ post(new Runnable() {
+ public void run() {
+ callShutterButtonFocus(pressed);
+ }
+ });
+ } else {
+ callShutterButtonFocus(pressed);
+ }
+ mOldPressed = pressed;
+ }
+ }
+
+ private void callShutterButtonFocus(boolean pressed) {
+ if (mListener != null) {
+ mListener.onShutterButtonFocus(this, pressed);
+ }
+ }
+
+ @Override
+ public boolean performClick() {
+ boolean result = super.performClick();
+ if (mListener != null) {
+ mListener.onShutterButtonClick(this);
+ }
+ return result;
+ }
+}
diff --git a/src/com/android/camera/SlideShow.java b/src/com/android/camera/SlideShow.java
new file mode 100644
index 0000000..23c7d4a
--- /dev/null
+++ b/src/com/android/camera/SlideShow.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2007 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;
+
+import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.os.Message;
+import android.view.View;
+import android.view.Window;
+import android.widget.ImageView;
+import android.widget.ViewSwitcher;
+import android.widget.Gallery.LayoutParams;
+
+import android.view.WindowManager;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import com.android.camera.ImageManager.IGetBitmap_cancelable;
+import com.android.camera.ImageManager.IImage;
+import com.android.camera.ImageManager.IImageList;
+
+import android.view.MotionEvent;
+
+public class SlideShow extends Activity implements ViewSwitcher.ViewFactory
+{
+ static final private String TAG = "SlideShow";
+ static final int sLag = 2000;
+ static final int sNextImageInterval = 3000;
+ private ImageManager.IImageList mImageList;
+ private int mCurrentPosition = 0;
+ private ImageView mSwitcher;
+ private boolean mPosted = false;
+
+ @Override
+ protected void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ Window wp = getWindow();
+ wp.setFlags(FLAG_KEEP_SCREEN_ON, FLAG_KEEP_SCREEN_ON);
+
+ setContentView(R.layout.slide_show);
+
+ mSwitcher = (ImageView)findViewById(R.id.imageview);
+ if (android.util.Config.LOGV)
+ Log.v(TAG, "mSwitcher " + mSwitcher);
+ }
+
+ @Override
+ protected void onResume()
+ {
+ super.onResume();
+
+ if (mImageList == null) {
+ mImageList = new FileImageList();
+ mCurrentPosition = 0;
+ }
+ loadImage();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ cancelPost();
+ }
+
+ static public class ImageViewTouch extends ImageView {
+ class xy {
+ public xy(float xIn, float yIn) {
+ x = xIn;
+ y = yIn;
+ timeAdded = System.currentTimeMillis();
+ }
+ public xy(MotionEvent e) {
+ x = e.getX();
+ y = e.getY();
+ timeAdded = System.currentTimeMillis();
+ }
+ float x,y;
+ long timeAdded;
+ }
+
+ SlideShow mSlideShow;
+ Paint mPaints[] = new Paint[1];
+ ArrayList<xy> mPoints = new ArrayList<xy>();
+ boolean mDown;
+
+ public ImageViewTouch(Context context) {
+ super(context);
+ mSlideShow = (SlideShow) context;
+ setScaleType(ImageView.ScaleType.CENTER);
+ setupPaint();
+ }
+
+ public ImageViewTouch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mSlideShow = (SlideShow) context;
+ setScaleType(ImageView.ScaleType.CENTER);
+ setupPaint();
+ }
+
+ private void setupPaint() {
+ for (int i = 0; i < mPaints.length; i++) {
+ Paint p = new Paint();
+ p.setARGB(255, 255, 255, 0);
+ p.setAntiAlias(true);
+ p.setStyle(Paint.Style.FILL);
+ p.setStrokeWidth(3F);
+ mPaints[i] = p;
+ }
+ }
+
+ private void addEvent(MotionEvent event) {
+ long now = System.currentTimeMillis();
+ mPoints.add(new xy(event));
+ for (int i = 0; i < event.getHistorySize(); i++)
+ mPoints.add(new xy(event.getHistoricalX(i), event.getHistoricalY(i)));
+ while (mPoints.size() > 0) {
+ xy ev = mPoints.get(0);
+ if (now - ev.timeAdded < sLag)
+ break;
+ mPoints.remove(0);
+ }
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ addEvent(event);
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDown = true;
+ mSlideShow.cancelPost();
+ postInvalidate();
+ break;
+ case MotionEvent.ACTION_UP:
+ mDown = false;
+ postInvalidate();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ mSlideShow.cancelPost();
+ postInvalidate();
+ break;
+ }
+ return true;
+ }
+
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ boolean didPaint = false;
+ long now = System.currentTimeMillis();
+ for (xy ev: mPoints) {
+ Paint p = mPaints[0];
+ long delta = now - ev.timeAdded;
+ if (delta > sLag)
+ continue;
+
+ int alpha2 = Math.max(0, 255 - (255 * (int)delta / sLag));
+ if (alpha2 == 0)
+ continue;
+ p.setAlpha(alpha2);
+ canvas.drawCircle(ev.x, ev.y, 2, p);
+ didPaint = true;
+ }
+ if (didPaint && !mDown)
+ postInvalidate();
+ }
+ }
+
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ cancelPost();
+ loadPreviousImage();
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ cancelPost();
+ loadNextImage();
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (mPosted)
+ cancelPost();
+ else
+ loadNextImage();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void cancelPost() {
+ mHandler.removeCallbacks(mNextImageRunnable);
+ mPosted = false;
+ }
+
+ private void post() {
+ mHandler.postDelayed(mNextImageRunnable, sNextImageInterval);
+ mPosted = true;
+ }
+
+ private void loadImage() {
+ ImageManager.IImage image = mImageList.getImageAt(mCurrentPosition);
+ if (image == null)
+ return;
+
+ Bitmap bitmap = image.thumbBitmap();
+ if (bitmap == null)
+ return;
+
+ mSwitcher.setImageDrawable(new BitmapDrawable(bitmap));
+ post();
+ }
+
+ private Runnable mNextImageRunnable = new Runnable() {
+ public void run() {
+ if (android.util.Config.LOGV)
+ Log.v(TAG, "mNextImagerunnable called");
+ loadNextImage();
+ }
+ };
+
+ private void loadNextImage() {
+ if (++mCurrentPosition >= mImageList.getCount())
+ mCurrentPosition = 0;
+ loadImage();
+ }
+
+ private void loadPreviousImage() {
+ if (mCurrentPosition == 0)
+ mCurrentPosition = mImageList.getCount() - 1;
+ else
+ mCurrentPosition -= 1;
+
+ loadImage();
+ }
+
+ public View makeView() {
+ ImageView i = new ImageView(this);
+ i.setBackgroundColor(0xFF000000);
+ i.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ i.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+ return i;
+ }
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ }
+ };
+
+ class FileImageList implements IImageList {
+ public HashMap<String, String> getBucketIds() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void commitChanges() {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void removeOnChangeListener(OnChange changeCallback) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void setOnChangeListener(OnChange changeCallback, Handler h) {
+ // TODO Auto-generated method stub
+
+ }
+
+ private ArrayList<FileImage> mImages = new ArrayList<FileImage>();
+ // image uri ==> Image object
+ private HashMap<Long, IImage> mCache = new HashMap<Long, IImage>();
+
+ class FileImage extends ImageManager.SimpleBaseImage {
+ long mId;
+ String mPath;
+
+ FileImage(long id, String path) {
+ mId = id;
+ mPath = path;
+ }
+
+ public long imageId() {
+ return mId;
+ }
+
+ public String getDataPath() {
+ return mPath;
+ }
+
+ public Bitmap fullSizeBitmap(int targetWidthOrHeight) {
+ return BitmapFactory.decodeFile(mPath);
+ }
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthOrHeight) {
+ return null;
+ }
+
+ public Bitmap thumbBitmap() {
+ Bitmap b = fullSizeBitmap(320);
+ Matrix m = new Matrix();
+ float scale = 320F / (float) b.getWidth();
+ m.setScale(scale, scale);
+ Bitmap scaledBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ return scaledBitmap;
+ }
+
+ public Bitmap miniThumbBitmap() {
+ return thumbBitmap();
+ }
+
+ public long fullSizeImageId() {
+ return mId;
+ }
+ }
+
+ private void enumerate(String path, ArrayList<String> list) {
+ File f = new File(path);
+ if (f.isDirectory()) {
+ String [] children = f.list();
+ if (children != null) {
+ for (int i = 0; i < children.length; i++) {
+ if (children[i].charAt(0) != '.')
+ enumerate(path + "/" + children[i], list);
+ }
+ }
+ } else {
+ if (path.endsWith(".jpeg") || path.endsWith(".jpg") || path.endsWith(".png")) {
+ if (f.length() > 0) {
+ list.add(path);
+ }
+ }
+ }
+ }
+
+ public FileImageList() {
+ ArrayList<String> list = new ArrayList<String>();
+ enumerate(Environment.getExternalStorageDirectory().getPath(), list);
+ enumerate("/data/images", list);
+
+ for (int i = 0; i < list.size(); i++) {
+ FileImage img = new FileImage(i, list.get(i));
+ mCache.put((long)i, img);
+ mImages.add(img);
+ }
+ }
+
+ public IImage getImageAt(int i) {
+ if (i >= mImages.size())
+ return null;
+
+ return mImages.get(i);
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ // TODO make this a hash lookup
+ int count = getCount();
+ for (int i = 0; i < count; i++) {
+ IImage image = getImageAt(i);
+ if (image.fullSizeImageUri().equals(uri)) {
+ return image;
+ }
+ }
+ return null;
+ }
+
+ public IImage getImageWithId(long id) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void removeImageAt(int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean removeImage(IImage image) {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getCount() {
+ return mImages.size();
+ }
+
+ public boolean isEmpty() {
+ return mImages.isEmpty();
+ }
+
+ public void deactivate() {
+ // nothing to do here
+ }
+ }
+
+}
diff --git a/src/com/android/camera/VideoCamera.java b/src/com/android/camera/VideoCamera.java
new file mode 100644
index 0000000..37e1dcb
--- /dev/null
+++ b/src/com/android/camera/VideoCamera.java
@@ -0,0 +1,1040 @@
+/*
+ * Copyright (C) 2007 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;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.location.LocationManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.os.StatFs;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Video;
+import android.text.format.DateFormat;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class VideoCamera extends Activity implements View.OnClickListener,
+ ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback, MediaRecorder.OnErrorListener {
+
+ private static final String TAG = "videocamera";
+
+ private static final boolean DEBUG = true;
+ private static final boolean DEBUG_SUPPRESS_AUDIO_RECORDING = DEBUG && false;
+ private static final boolean DEBUG_DO_NOT_REUSE_MEDIA_RECORDER = DEBUG && true;
+ private static final boolean DEBUG_LOG_APP_LIFECYCLE = DEBUG && false;
+
+ private static final int CLEAR_SCREEN_DELAY = 4;
+ private static final int UPDATE_RECORD_TIME = 5;
+
+ private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+ private static final long NO_STORAGE_ERROR = -1L;
+ private static final long CANNOT_STAT_ERROR = -2L;
+ private static final long LOW_STORAGE_THRESHOLD = 512L * 1024L;
+
+ public static final int MENU_SETTINGS = 6;
+ public static final int MENU_GALLERY_PHOTOS = 7;
+ public static final int MENU_GALLERY_VIDEOS = 8;
+ public static final int MENU_SAVE_GALLERY_PHOTO = 34;
+ public static final int MENU_SAVE_PLAY_VIDEO = 35;
+ public static final int MENU_SAVE_SELECT_VIDEO = 36;
+ public static final int MENU_SAVE_NEW_VIDEO = 37;
+
+ SharedPreferences mPreferences;
+
+ private static final float VIDEO_ASPECT_RATIO = 176.0f / 144.0f;
+ VideoPreview mVideoPreview;
+ SurfaceHolder mSurfaceHolder = null;
+ ImageView mBlackout = null;
+ ImageView mVideoFrame;
+ Bitmap mVideoFrameBitmap;
+
+ private MediaRecorder mMediaRecorder;
+ private boolean mMediaRecorderRecording = false;
+ private long mRecordingStartTime;
+ // The video file that the hardware camera is about to record into
+ // (or is recording into.)
+ private String mCameraVideoFilename;
+ private FileDescriptor mCameraVideoFileDescriptor;
+
+ // The video file that has already been recorded, and that is being
+ // examined by the user.
+ private String mCurrentVideoFilename;
+ private Uri mCurrentVideoUri;
+ private ContentValues mCurrentVideoValues;
+
+ boolean mPausing = false;
+
+ static ContentResolver mContentResolver;
+ boolean mDidRegister = false;
+
+ int mCurrentZoomIndex = 0;
+
+ private ShutterButton mShutterButton;
+ private TextView mRecordingTimeView;
+ private boolean mHasSdCard;
+
+ ArrayList<MenuItem> mGalleryItems = new ArrayList<MenuItem>();
+
+ View mPostPictureAlert;
+ LocationManager mLocationManager = null;
+
+ private Handler mHandler = new MainHandler();
+
+ /** This Handler is used to post message back onto the main thread of the application */
+ private class MainHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+
+ case CLEAR_SCREEN_DELAY: {
+ clearScreenOnFlag();
+ break;
+ }
+
+ case UPDATE_RECORD_TIME: {
+ if (mMediaRecorderRecording) {
+ long now = SystemClock.uptimeMillis();
+ long delta = now - mRecordingStartTime;
+ long seconds = delta / 1000;
+ long minutes = seconds / 60;
+ long hours = minutes / 60;
+ long remainderMinutes = minutes - (hours * 60);
+ long remainderSeconds = seconds - (minutes * 60);
+
+ String secondsString = Long.toString(remainderSeconds);
+ if (secondsString.length() < 2) {
+ secondsString = "0" + secondsString;
+ }
+ String minutesString = Long.toString(remainderMinutes);
+ if (minutesString.length() < 2) {
+ minutesString = "0" + minutesString;
+ }
+ String text = minutesString + ":" + secondsString;
+ if (hours > 0) {
+ String hoursString = Long.toString(hours);
+ if (hoursString.length() < 2) {
+ hoursString = "0" + hoursString;
+ }
+ text = hoursString + ":" + text;
+ }
+ mRecordingTimeView.setText(text);
+ // Work around a limitation of the T-Mobile G1: The T-Mobile
+ // hardware blitter can't pixel-accurately scale and clip at the same time,
+ // and the SurfaceFlinger doesn't attempt to work around this limitation.
+ // In order to avoid visual corruption we must manually refresh the entire
+ // surface view when changing any overlapping view's contents.
+ mVideoPreview.invalidate();
+ mHandler.sendEmptyMessageDelayed(UPDATE_RECORD_TIME, 1000);
+ }
+ break;
+ }
+
+ default:
+ Log.v(TAG, "Unhandled message: " + msg.what);
+ break;
+ }
+ }
+ };
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ mHasSdCard = false;
+ stopVideoRecording();
+ initializeVideo();
+ } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ // SD card available
+ // TODO put up a "please wait" message
+ // TODO also listen for the media scanner finished message
+ updateStorageHint();
+ mHasSdCard = true;
+ initializeVideo();
+ } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+ // SD card unavailable
+ updateStorageHint();
+ mHasSdCard = false;
+ releaseMediaRecorder();
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+ Toast.makeText(VideoCamera.this, getResources().getString(R.string.wait), 5000);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+ updateStorageHint();
+ }
+ }
+ };
+
+ static private String createName(long dateTaken) {
+ return DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken).toString();
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle) {
+ if (DEBUG_LOG_APP_LIFECYCLE) {
+ Log.v(TAG, "onCreate " + this.hashCode());
+ }
+ super.onCreate(icicle);
+
+ mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ mContentResolver = getContentResolver();
+
+ //setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
+ requestWindowFeature(Window.FEATURE_PROGRESS);
+ setContentView(R.layout.video_camera);
+
+ mVideoPreview = (VideoPreview) findViewById(R.id.camera_preview);
+ mVideoPreview.setAspectRatio(VIDEO_ASPECT_RATIO);
+
+ // don't set mSurfaceHolder here. We have it set ONLY within
+ // surfaceCreated / surfaceDestroyed, other parts of the code
+ // assume that when it is set, the surface is also set.
+ SurfaceHolder holder = mVideoPreview.getHolder();
+ holder.addCallback(this);
+ holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+
+ mBlackout = (ImageView) findViewById(R.id.blackout);
+ mBlackout.setBackgroundDrawable(new ColorDrawable(0xFF000000));
+
+ mPostPictureAlert = findViewById(R.id.post_picture_panel);
+
+ int[] ids = new int[]{R.id.play, R.id.share, R.id.discard,
+ R.id.cancel, R.id.attach};
+ for (int id : ids) {
+ findViewById(id).setOnClickListener(this);
+ }
+
+ mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
+ mShutterButton.setOnShutterButtonListener(this);
+ mRecordingTimeView = (TextView) findViewById(R.id.recording_time);
+ mVideoFrame = (ImageView) findViewById(R.id.video_frame);
+ }
+
+ @Override
+ public void onStart() {
+ if (DEBUG_LOG_APP_LIFECYCLE) {
+ Log.v(TAG, "onStart " + this.hashCode());
+ }
+ super.onStart();
+
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+ final boolean storageOK = getAvailableStorage() >= LOW_STORAGE_THRESHOLD;
+
+ if (!storageOK) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ updateStorageHint();
+ }
+ });
+ }
+ }
+ });
+ t.start();
+ }
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+
+ case R.id.gallery:
+ MenuHelper.gotoCameraVideoGallery(this);
+ break;
+
+ case R.id.attach:
+ doReturnToCaller(true);
+ break;
+
+ case R.id.cancel:
+ doReturnToCaller(false);
+ break;
+
+ case R.id.discard: {
+ discardCurrentVideoAndStartPreview();
+ break;
+ }
+
+ case R.id.share: {
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType("video/3gpp");
+ intent.putExtra(Intent.EXTRA_STREAM, mCurrentVideoUri);
+ try {
+ startActivity(Intent.createChooser(intent, getText(R.string.sendVideo)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(VideoCamera.this, R.string.no_way_to_share_video, Toast.LENGTH_SHORT).show();
+ }
+
+ break;
+ }
+
+ case R.id.play: {
+ doPlayCurrentVideo();
+ break;
+ }
+ }
+ }
+
+ public void onShutterButtonFocus(ShutterButton button, boolean pressed) {
+ switch (button.getId()) {
+ case R.id.shutter_button:
+ if (pressed) {
+ if (mMediaRecorderRecording) {
+ stopVideoRecordingAndDisplayDialog();
+ } else if (mVideoFrame.getVisibility() == View.VISIBLE) {
+ doStartCaptureMode();
+ } else {
+ startVideoRecording();
+ }
+ }
+ break;
+ }
+ }
+
+ public void onShutterButtonClick(ShutterButton button) {
+ // Do nothing (everything happens in onShutterButtonFocus).
+ }
+
+ private void doStartCaptureMode() {
+ if (isVideoCaptureIntent()) {
+ discardCurrentVideoAndStartPreview();
+ } else {
+ hideVideoFrameAndStartPreview();
+ }
+ }
+
+ private void doPlayCurrentVideo() {
+ Log.e(TAG, "Playing current video: " + mCurrentVideoUri);
+ Intent intent = new Intent(Intent.ACTION_VIEW, mCurrentVideoUri);
+ try {
+ startActivity(intent);
+ } catch (android.content.ActivityNotFoundException ex) {
+ Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
+ }
+ }
+
+ private void discardCurrentVideoAndStartPreview() {
+ deleteCurrentVideo();
+ hideVideoFrameAndStartPreview();
+ }
+
+ private OnScreenHint mStorageHint;
+
+ private void updateStorageHint() {
+ long remaining = getAvailableStorage();
+ String errorMessage = null;
+ if (remaining == NO_STORAGE_ERROR) {
+ errorMessage = getString(R.string.no_storage);
+ } else if (remaining < LOW_STORAGE_THRESHOLD) {
+ errorMessage = getString(R.string.spaceIsLow_content);
+ if (mStorageHint != null) {
+ mStorageHint.cancel();
+ mStorageHint = null;
+ }
+ }
+ if (errorMessage != null) {
+ if (mStorageHint == null) {
+ mStorageHint = OnScreenHint.makeText(this, errorMessage);
+ } else {
+ mStorageHint.setText(errorMessage);
+ }
+ mStorageHint.show();
+ } else if (mStorageHint != null) {
+ mStorageHint.cancel();
+ mStorageHint = null;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ if (DEBUG_LOG_APP_LIFECYCLE) {
+ Log.v(TAG, "onResume " + this.hashCode());
+ }
+ super.onResume();
+
+ setScreenTimeoutLong();
+
+ mPausing = false;
+
+ // install an intent filter to receive SD card related events.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
+ intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+ intentFilter.addDataScheme("file");
+ registerReceiver(mReceiver, intentFilter);
+ mDidRegister = true;
+ mHasSdCard = ImageManager.hasStorage();
+
+ mBlackout.setVisibility(View.INVISIBLE);
+ if (mVideoFrameBitmap == null) {
+ initializeVideo();
+ } else {
+ showPostRecordingAlert();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (DEBUG_LOG_APP_LIFECYCLE) {
+ Log.v(TAG, "onStop " + this.hashCode());
+ }
+ stopVideoRecording();
+ setScreenTimeoutSystemDefault();
+ super.onStop();
+ }
+
+ @Override
+ protected void onPause() {
+ if (DEBUG_LOG_APP_LIFECYCLE) {
+ Log.v(TAG, "onPause " + this.hashCode());
+ }
+ super.onPause();
+
+ stopVideoRecording();
+ hidePostPictureAlert();
+
+ mPausing = true;
+
+ if (mDidRegister) {
+ unregisterReceiver(mReceiver);
+ mDidRegister = false;
+ }
+ mBlackout.setVisibility(View.VISIBLE);
+ setScreenTimeoutSystemDefault();
+
+ if (mStorageHint != null) {
+ mStorageHint.cancel();
+ mStorageHint = null;
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ setScreenTimeoutLong();
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (mMediaRecorderRecording) {
+ Log.v(TAG, "onKeyBack");
+ stopVideoRecordingAndDisplayDialog();
+ return true;
+ } else if(isPostRecordingAlertVisible()) {
+ hideVideoFrameAndStartPreview();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_CAMERA:
+ if (event.getRepeatCount() == 0) {
+ // If we get a dpad center event without any focused view, move the
+ // focus to the shutter button and press it.
+ if (mShutterButton.isInTouchMode()) {
+ mShutterButton.requestFocusFromTouch();
+ } else {
+ mShutterButton.requestFocus();
+ }
+ mShutterButton.setPressed(true);
+ return true;
+ }
+ return true;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (event.getRepeatCount() == 0) {
+ // If we get a dpad center event without any focused view, move the
+ // focus to the shutter button and press it.
+ if (mShutterButton.isInTouchMode()) {
+ mShutterButton.requestFocusFromTouch();
+ } else {
+ mShutterButton.requestFocus();
+ }
+ mShutterButton.setPressed(true);
+ }
+ break;
+ case KeyEvent.KEYCODE_MENU:
+ if (mMediaRecorderRecording) {
+ stopVideoRecordingAndDisplayDialog();
+ return true;
+ }
+ break;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_CAMERA:
+ mShutterButton.setPressed(false);
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ stopVideoRecording();
+ initializeVideo();
+ }
+
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceHolder = holder;
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mSurfaceHolder = null;
+ }
+
+ void gotoGallery() {
+ MenuHelper.gotoCameraVideoGallery(this);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ for (int i = 1; i <= MenuHelper.MENU_ITEM_MAX; i++) {
+ if (i != MenuHelper.GENERIC_ITEM) {
+ menu.setGroupVisible(i, false);
+ }
+ }
+
+ menu.setGroupVisible(MenuHelper.VIDEO_MODE_ITEM, true);
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ if (isVideoCaptureIntent()) {
+ // No options menu for attach mode.
+ return false;
+ } else {
+ addBaseMenuItems(menu);
+ MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_ROTATE_MENU,
+ false,
+ VideoCamera.this,
+ mHandler,
+
+ // Handler for deletion
+ new Runnable() {
+ public void run() {
+ // What do we do here?
+ // mContentResolver.delete(uri, null, null);
+ }
+ },
+ new MenuHelper.MenuInvoker() {
+ public void run(final MenuHelper.MenuCallback cb) {
+ }
+ });
+
+ MenuItem gallery = menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_GALLERY_PHOTO, 0,
+ R.string.camera_gallery_photos_text).setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ gotoGallery();
+ return true;
+ }
+ });
+ gallery.setIcon(android.R.drawable.ic_menu_gallery);
+ }
+ return true;
+ }
+
+ private boolean isVideoCaptureIntent() {
+ String action = getIntent().getAction();
+ return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
+ }
+
+ private void doReturnToCaller(boolean success) {
+ Intent resultIntent = new Intent();
+ int resultCode;
+ if (success) {
+ resultCode = RESULT_OK;
+ resultIntent.setData(mCurrentVideoUri);
+ } else {
+ resultCode = RESULT_CANCELED;
+ }
+ setResult(resultCode, resultIntent);
+ finish();
+ }
+
+ /**
+ * Returns
+ * @return number of bytes available, or an ERROR code.
+ */
+ private static long getAvailableStorage() {
+ try {
+ if (!ImageManager.hasStorage()) {
+ return NO_STORAGE_ERROR;
+ } else {
+ String storageDirectory = Environment.getExternalStorageDirectory().toString();
+ StatFs stat = new StatFs(storageDirectory);
+ return ((long)stat.getAvailableBlocks() * (long)stat.getBlockSize());
+ }
+ } catch (Exception ex) {
+ // if we can't stat the filesystem then we don't know how many
+ // free bytes exist. It might be zero but just leave it
+ // blank since we really don't know.
+ return CANNOT_STAT_ERROR;
+ }
+ }
+
+ private void cleanupEmptyFile() {
+ if (mCameraVideoFilename != null) {
+ File f = new File(mCameraVideoFilename);
+ if (f.length() == 0 && f.delete()) {
+ Log.v(TAG, "Empty video file deleted: " + mCameraVideoFilename);
+ mCameraVideoFilename = null;
+ }
+ }
+ }
+
+ // Returns false if initializeVideo fails
+ private boolean initializeVideo() {
+ Log.v(TAG, "initializeVideo");
+ boolean isCaptureIntent = isVideoCaptureIntent();
+ Intent intent = getIntent();
+ Bundle myExtras = intent.getExtras();
+
+ if (isCaptureIntent && myExtras != null) {
+ Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+ if (saveUri != null) {
+ try {
+ mCameraVideoFileDescriptor = mContentResolver.
+ openFileDescriptor(saveUri, "rw").getFileDescriptor();
+ mCurrentVideoUri = saveUri;
+ }
+ catch (java.io.FileNotFoundException ex) {
+ // invalid uri
+ Log.e(TAG, ex.toString());
+ }
+ }
+ }
+ releaseMediaRecorder();
+
+ if (mSurfaceHolder == null) {
+ Log.v(TAG, "SurfaceHolder is null");
+ return false;
+ }
+
+ mMediaRecorder = new MediaRecorder();
+
+ if (DEBUG_SUPPRESS_AUDIO_RECORDING) {
+ Log.v(TAG, "DEBUG_SUPPRESS_AUDIO_RECORDING is true.");
+ } else {
+ mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ }
+ mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+ mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
+
+ if (!mHasSdCard) {
+ mMediaRecorder.setOutputFile("/dev/null");
+ } else {
+ // We try Uri in intent first. If it doesn't work, use our own instead.
+ if (mCameraVideoFileDescriptor != null) {
+ mMediaRecorder.setOutputFile(mCameraVideoFileDescriptor);
+ } else {
+ createVideoPath();
+ mMediaRecorder.setOutputFile(mCameraVideoFilename);
+ }
+ }
+
+ boolean videoQualityHigh = getBooleanPreference(CameraSettings.KEY_VIDEO_QUALITY,
+ CameraSettings.DEFAULT_VIDEO_QUALITY_VALUE);
+
+ if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
+ int extraVideoQuality = intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
+ videoQualityHigh = (extraVideoQuality > 0);
+ }
+
+ // Use the same frame rate for both, since internally
+ // if the frame rate is too large, it can cause camera to become
+ // unstable. We need to fix the MediaRecorder to disable the support
+ // of setting frame rate for now.
+ mMediaRecorder.setVideoFrameRate(20);
+ if (videoQualityHigh) {
+ mMediaRecorder.setVideoSize(352,288);
+ } else {
+ mMediaRecorder.setVideoSize(176,144);
+ }
+ mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263);
+ if (!DEBUG_SUPPRESS_AUDIO_RECORDING) {
+ mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
+ }
+ mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
+ try {
+ mMediaRecorder.prepare();
+ } catch (IOException exception) {
+ Log.e(TAG, "prepare failed for " + mCameraVideoFilename);
+ releaseMediaRecorder();
+ // TODO: add more exception handling logic here
+ return false;
+ }
+ mMediaRecorderRecording = false;
+ return true;
+ }
+
+ private void releaseMediaRecorder() {
+ Log.v(TAG, "Releasing media recorder.");
+ if (mMediaRecorder != null) {
+ cleanupEmptyFile();
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+ }
+ }
+
+ private void restartPreview() {
+ if (DEBUG_DO_NOT_REUSE_MEDIA_RECORDER) {
+ Log.v(TAG, "DEBUG_DO_NOT_REUSE_MEDIA_RECORDER recreating mMediaRecorder.");
+ initializeVideo();
+ } else {
+ try {
+ mMediaRecorder.prepare();
+ } catch (IOException exception) {
+ Log.e(TAG, "prepare failed for " + mCameraVideoFilename);
+ releaseMediaRecorder();
+ // TODO: add more exception handling logic here
+ }
+ }
+ }
+
+ private int getIntPreference(String key, int defaultValue) {
+ String s = mPreferences.getString(key, "");
+ int result = defaultValue;
+ try {
+ result = Integer.parseInt(s);
+ } catch (NumberFormatException e) {
+ // Ignore, result is already the default value.
+ }
+ return result;
+ }
+
+ private boolean getBooleanPreference(String key, boolean defaultValue) {
+ return getIntPreference(key, defaultValue ? 1 : 0) != 0;
+ }
+
+ private void createVideoPath() {
+ long dateTaken = System.currentTimeMillis();
+ String title = createName(dateTaken);
+ String displayName = title + ".3gp"; // Used when emailing.
+ String cameraDirPath = ImageManager.CAMERA_IMAGE_BUCKET_NAME;
+ File cameraDir = new File(cameraDirPath);
+ cameraDir.mkdirs();
+ SimpleDateFormat dateFormat = new SimpleDateFormat(
+ getString(R.string.video_file_name_format));
+ Date date = new Date(dateTaken);
+ String filepart = dateFormat.format(date);
+ String filename = cameraDirPath + "/" + filepart + ".3gp";
+ ContentValues values = new ContentValues(7);
+ values.put(Video.Media.TITLE, title);
+ values.put(Video.Media.DISPLAY_NAME, displayName);
+ values.put(Video.Media.DESCRIPTION, "");
+ values.put(Video.Media.DATE_TAKEN, dateTaken);
+ values.put(Video.Media.MIME_TYPE, "video/3gpp");
+ values.put(Video.Media.DATA, filename);
+ mCameraVideoFilename = filename;
+ Log.v(TAG, "Current camera video filename: " + mCameraVideoFilename);
+ mCurrentVideoValues = values;
+ }
+
+ private void registerVideo() {
+ if (mCameraVideoFileDescriptor == null) {
+ Uri videoTable = Uri.parse("content://media/external/video/media");
+ mCurrentVideoUri = mContentResolver.insert(videoTable,
+ mCurrentVideoValues);
+ Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
+ }
+ mCurrentVideoValues = null;
+ }
+
+ private void deleteCurrentVideo() {
+ if (mCurrentVideoFilename != null) {
+ deleteVideoFile(mCurrentVideoFilename);
+ mCurrentVideoFilename = null;
+ }
+ if (mCurrentVideoUri != null) {
+ mContentResolver.delete(mCurrentVideoUri, null, null);
+ mCurrentVideoUri = null;
+ }
+ }
+
+ private void deleteVideoFile(String fileName) {
+ Log.v(TAG, "Deleting video " + fileName);
+ File f = new File(fileName);
+ if (! f.delete()) {
+ Log.v(TAG, "Could not delete " + fileName);
+ }
+ }
+
+ private void addBaseMenuItems(Menu menu) {
+ MenuHelper.addSwitchModeMenuItem(menu, this, false);
+ {
+ MenuItem gallery = menu.add(MenuHelper.IMAGE_MODE_ITEM, MENU_GALLERY_PHOTOS, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ gotoGallery();
+ return true;
+ }
+ });
+ gallery.setIcon(android.R.drawable.ic_menu_gallery);
+ mGalleryItems.add(gallery);
+ }
+ {
+ MenuItem gallery = menu.add(MenuHelper.VIDEO_MODE_ITEM, MENU_GALLERY_VIDEOS, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ gotoGallery();
+ return true;
+ }
+ });
+ gallery.setIcon(android.R.drawable.ic_menu_gallery);
+ mGalleryItems.add(gallery);
+ }
+
+ MenuItem item = menu.add(MenuHelper.GENERIC_ITEM, MENU_SETTINGS, 0, R.string.settings).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent intent = new Intent();
+ intent.setClass(VideoCamera.this, CameraSettings.class);
+ startActivity(intent);
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+ }
+
+ // from MediaRecorder.OnErrorListener
+ public void onError(MediaRecorder mr, int what, int extra) {
+ if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
+ // We may have run out of space on the sdcard.
+ stopVideoRecording();
+ updateStorageHint();
+ }
+ }
+
+ private void startVideoRecording() {
+ Log.v(TAG, "startVideoRecording");
+ if (!mMediaRecorderRecording) {
+
+ if (mStorageHint != null) {
+ Log.v(TAG, "Storage issue, ignore the start request");
+ return;
+ }
+
+ // Check mMediaRecorder to see whether it is initialized or not.
+ if (mMediaRecorder == null && initializeVideo() == false ) {
+ Log.e(TAG, "Initialize video (MediaRecorder) failed.");
+ return;
+ }
+
+ try {
+ mMediaRecorder.setOnErrorListener(this);
+ mMediaRecorder.start(); // Recording is now started
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Could not start media recorder. ", e);
+ return;
+ }
+ mMediaRecorderRecording = true;
+ mRecordingStartTime = SystemClock.uptimeMillis();
+ updateRecordingIndicator(true);
+ mRecordingTimeView.setText("");
+ mRecordingTimeView.setVisibility(View.VISIBLE);
+ mHandler.sendEmptyMessage(UPDATE_RECORD_TIME);
+ setScreenTimeoutInfinite();
+ }
+ }
+
+ private void updateRecordingIndicator(boolean showRecording) {
+ int drawableId = showRecording ? R.drawable.ic_camera_bar_indicator_record
+ : R.drawable.ic_camera_indicator_video;
+ Drawable drawable = getResources().getDrawable(drawableId);
+ mShutterButton.setImageDrawable(drawable);
+ }
+
+ private void stopVideoRecordingAndDisplayDialog() {
+ Log.v(TAG, "stopVideoRecordingAndDisplayDialog");
+ if (mMediaRecorderRecording) {
+ stopVideoRecording();
+ acquireAndShowVideoFrame();
+ showPostRecordingAlert();
+ }
+ }
+
+ private void showPostRecordingAlert() {
+ int[] pickIds = {R.id.attach, R.id.cancel};
+ int[] normalIds = {R.id.gallery, R.id.share, R.id.discard};
+ int[] alwaysOnIds = {R.id.play};
+ int[] hideIds = pickIds;
+ int[] connectIds = normalIds;
+ if (isVideoCaptureIntent()) {
+ hideIds = normalIds;
+ connectIds = pickIds;
+ }
+ for(int id : hideIds) {
+ mPostPictureAlert.findViewById(id).setVisibility(View.GONE);
+ }
+ connectAndFadeIn(connectIds);
+ connectAndFadeIn(alwaysOnIds);
+ mPostPictureAlert.setVisibility(View.VISIBLE);
+ }
+
+ private void connectAndFadeIn(int[] connectIds) {
+ for(int id : connectIds) {
+ View view = mPostPictureAlert.findViewById(id);
+ view.setOnClickListener(this);
+ Animation animation = new AlphaAnimation(0F, 1F);
+ animation.setDuration(500);
+ view.setAnimation(animation);
+ }
+ }
+
+ private void hidePostPictureAlert() {
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ }
+
+ private boolean isPostRecordingAlertVisible() {
+ return mPostPictureAlert.getVisibility() == View.VISIBLE;
+ }
+
+ private void stopVideoRecording() {
+ Log.v(TAG, "stopVideoRecording");
+ boolean needToRegisterRecording = false;
+ if (mMediaRecorderRecording || mMediaRecorder != null) {
+ if (mMediaRecorderRecording && mMediaRecorder != null) {
+ try {
+ mMediaRecorder.setOnErrorListener(null);
+ mMediaRecorder.stop();
+ } catch (RuntimeException e) {
+ Log.e(TAG, "stop fail: " + e.getMessage());
+ }
+ mCurrentVideoFilename = mCameraVideoFilename;
+ Log.v(TAG, "Setting current video filename: " + mCurrentVideoFilename);
+ needToRegisterRecording = true;
+ mMediaRecorderRecording = false;
+ }
+ releaseMediaRecorder();
+ updateRecordingIndicator(false);
+ mRecordingTimeView.setVisibility(View.GONE);
+ setScreenTimeoutLong();
+ }
+ if (needToRegisterRecording && mHasSdCard) registerVideo();
+
+ mCameraVideoFilename = null;
+ mCameraVideoFileDescriptor = null;
+ }
+
+ private void setScreenTimeoutSystemDefault() {
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ clearScreenOnFlag();
+ }
+
+ private void setScreenTimeoutLong() {
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ setScreenOnFlag();
+ mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+ }
+
+ private void setScreenTimeoutInfinite() {
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ setScreenOnFlag();
+ }
+
+ private void clearScreenOnFlag() {
+ Window w = getWindow();
+ final int keepScreenOnFlag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ if ((w.getAttributes().flags & keepScreenOnFlag) != 0) {
+ w.clearFlags(keepScreenOnFlag);
+ }
+ }
+
+ private void setScreenOnFlag() {
+ Window w = getWindow();
+ final int keepScreenOnFlag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ if ((w.getAttributes().flags & keepScreenOnFlag) == 0) {
+ w.addFlags(keepScreenOnFlag);
+ }
+ }
+
+ private void hideVideoFrameAndStartPreview() {
+ hidePostPictureAlert();
+ hideVideoFrame();
+ restartPreview();
+ }
+
+ private void acquireAndShowVideoFrame() {
+ recycleVideoFrameBitmap();
+ mVideoFrameBitmap = ImageManager.createVideoThumbnail(mCurrentVideoFilename);
+ mVideoFrame.setImageBitmap(mVideoFrameBitmap);
+ mVideoFrame.setVisibility(View.VISIBLE);
+ }
+
+ private void hideVideoFrame() {
+ recycleVideoFrameBitmap();
+ mVideoFrame.setVisibility(View.GONE);
+ }
+
+ private void recycleVideoFrameBitmap() {
+ if (mVideoFrameBitmap != null) {
+ mVideoFrame.setImageDrawable(null);
+ mVideoFrameBitmap.recycle();
+ mVideoFrameBitmap = null;
+ }
+ }
+}
+
diff --git a/src/com/android/camera/VideoPreview.java b/src/com/android/camera/VideoPreview.java
new file mode 100644
index 0000000..aed1e89
--- /dev/null
+++ b/src/com/android/camera/VideoPreview.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceView;
+import android.view.View.MeasureSpec;
+
+class VideoPreview extends SurfaceView {
+ private float mAspectRatio;
+ private int mHorizontalTileSize = 1;
+ private int mVerticalTileSize = 1;
+
+ /**
+ * Setting the aspect ratio to this value means to not enforce an aspect ratio.
+ */
+ public static float DONT_CARE = 0.0f;
+
+ public VideoPreview(Context context) {
+ super(context);
+ }
+
+ public VideoPreview(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public VideoPreview(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setTileSize(int horizontalTileSize, int verticalTileSize) {
+ if ((mHorizontalTileSize != horizontalTileSize)
+ || (mVerticalTileSize != verticalTileSize)) {
+ mHorizontalTileSize = horizontalTileSize;
+ mVerticalTileSize = verticalTileSize;
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ public void setAspectRatio(int width, int height) {
+ setAspectRatio(((float) width) / ((float) height));
+ }
+
+ public void setAspectRatio(float aspectRatio) {
+ if (mAspectRatio != aspectRatio) {
+ mAspectRatio = aspectRatio;
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mAspectRatio != DONT_CARE) {
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ int width = widthSpecSize;
+ int height = heightSpecSize;
+
+ if (width > 0 && height > 0) {
+ float defaultRatio = ((float) width) / ((float) height);
+ if (defaultRatio < mAspectRatio) {
+ // Need to reduce height
+ height = (int) (width / mAspectRatio);
+ } else if (defaultRatio > mAspectRatio) {
+ width = (int) (height * mAspectRatio);
+ }
+ width = roundUpToTile(width, mHorizontalTileSize, widthSpecSize);
+ height = roundUpToTile(height, mVerticalTileSize, heightSpecSize);
+ Log.i("VideoPreview", "ar " + mAspectRatio + " setting size: " + width + 'x' + height);
+ setMeasuredDimension(width, height);
+ return;
+ }
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ private int roundUpToTile(int dimension, int tileSize, int maxDimension) {
+ return Math.min(((dimension + tileSize - 1) / tileSize) * tileSize, maxDimension);
+ }
+}
diff --git a/src/com/android/camera/ViewImage.java b/src/com/android/camera/ViewImage.java
new file mode 100644
index 0000000..07d396f
--- /dev/null
+++ b/src/com/android/camera/ViewImage.java
@@ -0,0 +1,1677 @@
+/*
+ * Copyright (C) 2007 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;
+
+import java.util.Random;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.LinearLayout;
+import android.widget.Scroller;
+import android.widget.Toast;
+import android.widget.ZoomButtonsController;
+import android.widget.ZoomRingController;
+
+import com.android.camera.ImageManager.IImage;
+
+public class ViewImage extends Activity implements View.OnClickListener
+{
+ static final String TAG = "ViewImage";
+ private ImageGetter mGetter;
+
+ static final boolean sSlideShowHidesStatusBar = true;
+
+ // Choices for what adjacents to load.
+ static private final int[] sOrder_adjacents = new int[] { 0, 1, -1 };
+ static private final int[] sOrder_slideshow = new int[] { 0 };
+
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ }
+ };
+
+ private Random mRandom = new Random(System.currentTimeMillis());
+ private int [] mShuffleOrder;
+ private boolean mUseShuffleOrder = false;
+ private boolean mSlideShowLoop = false;
+
+ private static final int MODE_NORMAL = 1;
+ private static final int MODE_SLIDESHOW = 2;
+ private int mMode = MODE_NORMAL;
+ private boolean mFullScreenInNormalMode;
+ private boolean mShowActionIcons;
+ private View mActionIconPanel;
+ private View mShutterButton;
+
+ private boolean mSortAscending = false;
+ private int mSlideShowInterval;
+ private int mLastSlideShowImage;
+ private boolean mFirst = true;
+ private int mCurrentPosition = 0;
+ private boolean mLayoutComplete = false;
+
+ // represents which style animation to use
+ private int mAnimationIndex;
+ private Animation [] mSlideShowInAnimation;
+ private Animation [] mSlideShowOutAnimation;
+
+ private SharedPreferences mPrefs;
+
+ private View mNextImageView, mPrevImageView;
+ private Animation mHideNextImageViewAnimation = new AlphaAnimation(1F, 0F);
+ private Animation mHidePrevImageViewAnimation = new AlphaAnimation(1F, 0F);
+ private Animation mShowNextImageViewAnimation = new AlphaAnimation(0F, 1F);
+ private Animation mShowPrevImageViewAnimation = new AlphaAnimation(0F, 1F);
+
+
+ static final int sPadding = 20;
+ static final int sHysteresis = sPadding * 2;
+ static final int sBaseScrollDuration = 1000; // ms
+
+ private ImageManager.IImageList mAllImages;
+
+ private int mSlideShowImageCurrent = 0;
+ private ImageViewTouch [] mSlideShowImageViews = new ImageViewTouch[2];
+
+
+ // Array of image views. The center view is the one the user is focused
+ // on. The one at the zeroth position and the second position reflect
+ // the images to the left/right of the center image.
+ private ImageViewTouch[] mImageViews = new ImageViewTouch[3];
+
+ // Container for the three image views. This guy can be "scrolled"
+ // to reveal the image prior to and after the center image.
+ private ScrollHandler mScroller;
+
+ private MenuHelper.MenuItemsResult mImageMenuRunnable;
+
+ private Runnable mDismissOnScreenControlsRunnable;
+ private boolean mCameraReviewMode;
+
+ private int mCurrentOrientation;
+
+
+ public ViewImage() {
+ }
+
+ private void updateNextPrevControls() {
+ boolean showPrev = mCurrentPosition > 0;
+ boolean showNext = mCurrentPosition < mAllImages.getCount() - 1;
+
+ boolean prevIsVisible = mPrevImageView.getVisibility() == View.VISIBLE;
+ boolean nextIsVisible = mNextImageView.getVisibility() == View.VISIBLE;
+
+ if (showPrev && !prevIsVisible) {
+ Animation a = mShowPrevImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mPrevImageView.setAnimation(a);
+ mPrevImageView.setVisibility(View.VISIBLE);
+ } else if (!showPrev && prevIsVisible) {
+ Animation a = mHidePrevImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mPrevImageView.setAnimation(a);
+ mPrevImageView.setVisibility(View.GONE);
+ }
+
+ if (showNext && !nextIsVisible) {
+ Animation a = mShowNextImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mNextImageView.setAnimation(a);
+ mNextImageView.setVisibility(View.VISIBLE);
+ } else if (!showNext && nextIsVisible) {
+ Animation a = mHideNextImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mNextImageView.setAnimation(a);
+ mNextImageView.setVisibility(View.GONE);
+ }
+ }
+
+ private void showOnScreenControls() {
+ updateNextPrevControls();
+ scheduleDismissOnScreenControls();
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent m) {
+ boolean sup = super.dispatchTouchEvent(m);
+ if (sup == false) {
+ if (mMode == MODE_SLIDESHOW) {
+ mSlideShowImageViews[mSlideShowImageCurrent].handleTouchEvent(m);
+ } else if (mMode == MODE_NORMAL){
+ mImageViews[1].handleTouchEvent(m);
+ }
+ return true;
+ }
+ return true;
+ }
+
+ private void scheduleDismissOnScreenControls() {
+ mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
+ mHandler.postDelayed(mDismissOnScreenControlsRunnable, 1500);
+ }
+
+ public void setupDismissOnScreenControlRunnable() {
+ mDismissOnScreenControlsRunnable = new Runnable() {
+ public void run() {
+ if (!mShowActionIcons) {
+ if (mNextImageView.getVisibility() == View.VISIBLE) {
+ Animation a = mHideNextImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mNextImageView.setAnimation(a);
+ mNextImageView.setVisibility(View.INVISIBLE);
+ }
+
+ if (mPrevImageView.getVisibility() == View.VISIBLE) {
+ Animation a = mHidePrevImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mPrevImageView.setAnimation(a);
+ mPrevImageView.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+ };
+ }
+
+ private boolean isPickIntent() {
+ String action = getIntent().getAction();
+ return (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action));
+ }
+
+ private static final boolean sUseBounce = false;
+ private static final boolean sAnimateTransitions = false;
+
+ static public class ImageViewTouch extends ImageViewTouchBase {
+ private ViewImage mViewImage;
+ private boolean mEnableTrackballScroll;
+ private GestureDetector mGestureDetector;
+
+ private static int TOUCH_AREA_WIDTH = 60;
+
+ // The zoom ring setup:
+ // We limit the thumb on the zoom ring in the range 0 to 5/3*PI. The
+ // last PI/3 of the ring is left as a region that the thumb can't go in.
+ // The 5/3*PI range is divided into 60 steps. Each step scales the image
+ // by mScaleRate. We make mScaleRate^60 = maxZoom().
+
+ // This is the max step value we can have for the zoom ring.
+ private static int MAX_STEP = 60;
+ // This is the angle we used to separate each step.
+ private static float STEP_ANGLE = (5 * (float) Math.PI / 3) / MAX_STEP;
+ // The scale rate for each step.
+ private float mScaleRate;
+
+ // Returns current scale step (numbered from 0 to MAX_STEP).
+ private int getCurrentStep() {
+ float s = getScale();
+ float b = mScaleRate;
+ int step = (int)Math.round(Math.log(s) / Math.log(b));
+ return Math.max(0, Math.min(MAX_STEP, step));
+ }
+
+ // Limit the thumb on the zoom ring in the range 0 to 5/3*PI. (clockwise
+ // angle is negative, and we need to mod 2*PI for the API to work.)
+ private void setZoomRingBounds() {
+ mScaleRate = (float) Math.pow(maxZoom(), 1.0 / MAX_STEP);
+ float limit = (2 - 5 / 3F) * (float) Math.PI;
+ mZoomRingController.setThumbClockwiseBound(limit);
+ mZoomRingController.setThumbCounterclockwiseBound(0);
+ }
+
+ private ZoomButtonsController mZoomButtonsController;
+
+ // The zoom ring is set to visible by a double tap.
+ private ZoomRingController mZoomRingController;
+ private ZoomRingController.OnZoomListener mZoomListener =
+ new ZoomRingController.OnZoomListener() {
+ public void onCenter(int x, int y) {
+ }
+
+ public void onBeginPan() {
+ }
+
+ public boolean onPan(int deltaX, int deltaY) {
+ postTranslate(-deltaX, -deltaY, sUseBounce);
+ ImageViewTouch.this.center(true, true, false);
+ return true;
+ }
+
+ public void onEndPan() {
+ }
+
+ // The clockwise angle is negative, so we need to mod 2*PI
+ private float stepToAngle(int step) {
+ float angle = step * STEP_ANGLE;
+ angle = (float) Math.PI * 2 - angle;
+ return angle;
+ }
+
+ private int angleToStep(double angle) {
+ angle = Math.PI * 2 - angle;
+ int step = (int)Math.round(angle / STEP_ANGLE);
+ return step;
+ }
+
+ public void onVisibilityChanged(boolean visible) {
+ if (visible) {
+ int step = getCurrentStep();
+ float angle = stepToAngle(step);
+ mZoomRingController.setThumbAngle(angle);
+ }
+ }
+
+ public void onBeginDrag() {
+ setZoomRingBounds();
+ }
+
+ public void onEndDrag() {
+ }
+
+ public boolean onDragZoom(int deltaZoomLevel, int centerX,
+ int centerY, float startAngle, float curAngle) {
+ setZoomRingBounds();
+ int deltaStep = angleToStep(curAngle) - getCurrentStep();
+ if ((deltaZoomLevel > 0) && (deltaStep < 0)) return false;
+ if ((deltaZoomLevel < 0) && (deltaStep > 0)) return false;
+ if ((deltaZoomLevel == 0) || (deltaStep == 0)) return false;
+
+ float oldScale = getScale();
+
+ // First move centerX/centerY to the center of the view.
+ int deltaX = getWidth() / 2 - centerX;
+ int deltaY = getHeight() / 2 - centerY;
+ panBy(deltaX, deltaY);
+
+ // Do zoom in/out.
+ if (deltaStep > 0) {
+ zoomIn((float) Math.pow(mScaleRate, deltaStep));
+ } else if (deltaStep < 0) {
+ zoomOut((float) Math.pow(mScaleRate, -deltaStep));
+ }
+
+ // Reverse the first centering.
+ panBy(-deltaX, -deltaY);
+
+ // Return true if the zoom succeeds.
+ return (oldScale != getScale());
+ }
+
+ public void onSimpleZoom(boolean zoomIn) {
+ if (zoomIn) zoomIn();
+ else zoomOut();
+ }
+ };
+
+ public ImageViewTouch(Context context) {
+ super(context);
+ setup(context);
+ }
+
+ public ImageViewTouch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setup(context);
+ }
+
+ private void setup(Context context) {
+ mViewImage = (ViewImage) context;
+ mZoomRingController = new ZoomRingController(context, this);
+ mZoomRingController.setVibration(false);
+ mZoomRingController.setZoomCallbackThreshold(STEP_ANGLE);
+ mZoomRingController.setResetThumbAutomatically(false);
+ mZoomRingController.setCallback(mZoomListener);
+ mGestureDetector = new GestureDetector(getContext(), new MyGestureListener());
+ mGestureDetector.setOnDoubleTapListener(new MyDoubleTapListener());
+ mZoomButtonsController = new ZoomButtonsController(context, this);
+ mZoomButtonsController.setOverviewVisible(false);
+ mZoomButtonsController.setCallback(new ZoomButtonsController.OnZoomListener() {
+
+ public void onCenter(int x, int y) {
+ mZoomListener.onCenter(x, y);
+ }
+
+ public void onOverview() {
+ }
+
+ public void onVisibilityChanged(boolean visible) {
+ mZoomListener.onVisibilityChanged(visible);
+ }
+
+ public void onZoom(boolean zoomIn) {
+ mZoomListener.onSimpleZoom(zoomIn);
+ }
+ });
+ }
+
+ public void setEnableTrackballScroll(boolean enable) {
+ mEnableTrackballScroll = enable;
+ }
+
+ protected void postTranslate(float dx, float dy, boolean bounceOK) {
+ super.postTranslate(dx, dy);
+ if (dx != 0F || dy != 0F)
+ mViewImage.showOnScreenControls();
+
+ if (!sUseBounce) {
+ center(true, false, false);
+ }
+ }
+
+ protected ScrollHandler scrollHandler() {
+ return mViewImage.mScroller;
+ }
+
+ public boolean handleTouchEvent(MotionEvent m) {
+ return mGestureDetector.onTouchEvent(m);
+ }
+
+ private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
+ public boolean onScroll(MotionEvent e1, MotionEvent e2,
+ float distanceX, float distanceY) {
+ if (getScale() > 1F) {
+ postTranslate(-distanceX, -distanceY, sUseBounce);
+ ImageViewTouch.this.center(true, true, false);
+ }
+ return true;
+ }
+ }
+
+ private class MyDoubleTapListener implements GestureDetector.OnDoubleTapListener {
+ // On single tap, we show the arrows. We also change to the
+ // prev/next image if the user taps on the left/right region.
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ ViewImage viewImage = mViewImage;
+
+ int viewWidth = getWidth();
+ int x = (int) e.getX();
+ int y = (int) e.getY();
+ if (x < TOUCH_AREA_WIDTH) {
+ viewImage.moveNextOrPrevious(-1);
+ } else if (x > viewWidth - TOUCH_AREA_WIDTH) {
+ viewImage.moveNextOrPrevious(1);
+ }
+
+ viewImage.setMode(MODE_NORMAL);
+ viewImage.showOnScreenControls();
+
+ return true;
+ }
+
+ // On double tap, we show the zoom ring control.
+ public boolean onDoubleTapEvent(MotionEvent e) {
+ mViewImage.setMode(MODE_NORMAL);
+ mZoomRingController.handleDoubleTapEvent(e);
+ mZoomButtonsController.handleDoubleTapEvent(e);
+ return true;
+ }
+
+ public boolean onDoubleTap(MotionEvent e) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event)
+ {
+ // Don't respond to arrow keys if trackball scrolling is not enabled
+ if (!mEnableTrackballScroll) {
+ if ((keyCode >= KeyEvent.KEYCODE_DPAD_UP)
+ && (keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT)) {
+ return super.onKeyDown(keyCode, event);
+ }
+ }
+
+ int current = mViewImage.mCurrentPosition;
+
+ int nextImagePos = -2; // default no next image
+ try {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER: {
+ if (mViewImage.isPickIntent()) {
+ ImageManager.IImage img = mViewImage.mAllImages.getImageAt(mViewImage.mCurrentPosition);
+ mViewImage.setResult(RESULT_OK,
+ new Intent().setData(img.fullSizeImageUri()));
+ mViewImage.finish();
+ }
+ break;
+ }
+ case KeyEvent.KEYCODE_DPAD_LEFT: {
+ panBy(sPanRate, 0);
+ int maxOffset = (current == 0) ? 0 : sHysteresis;
+ if (getScale() <= 1F || isShiftedToNextImage(true, maxOffset)) {
+ nextImagePos = current - 1;
+ } else {
+ center(true, false, true);
+ }
+ return true;
+ }
+ case KeyEvent.KEYCODE_DPAD_RIGHT: {
+ panBy(-sPanRate, 0);
+ int maxOffset = (current == mViewImage.mAllImages.getCount()-1) ? 0 : sHysteresis;
+ if (getScale() <= 1F || isShiftedToNextImage(false, maxOffset)) {
+ nextImagePos = current + 1;
+ } else {
+ center(true, false, true);
+ }
+ return true;
+ }
+ case KeyEvent.KEYCODE_DPAD_UP: {
+ panBy(0, sPanRate);
+ center(true, false, false);
+ return true;
+ }
+ case KeyEvent.KEYCODE_DPAD_DOWN: {
+ panBy(0, -sPanRate);
+ center(true, false, false);
+ return true;
+ }
+ case KeyEvent.KEYCODE_DEL:
+ MenuHelper.deletePhoto(mViewImage, mViewImage.mDeletePhotoRunnable);
+ break;
+ }
+ } finally {
+ if (nextImagePos >= 0 && nextImagePos < mViewImage.mAllImages.getCount()) {
+ synchronized (mViewImage) {
+ mViewImage.setMode(MODE_NORMAL);
+ mViewImage.setImage(nextImagePos);
+ }
+ } else if (nextImagePos != -2) {
+ center(true, true, false);
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ protected boolean isShiftedToNextImage(boolean left, int maxOffset) {
+ boolean retval;
+ Bitmap bitmap = mBitmapDisplayed;
+ Matrix m = getImageViewMatrix();
+ if (left) {
+ float [] t1 = new float[] { 0, 0 };
+ m.mapPoints(t1);
+ retval = t1[0] > maxOffset;
+ } else {
+ int width = bitmap != null ? bitmap.getWidth() : getWidth();
+ float [] t1 = new float[] { width, 0 };
+ m.mapPoints(t1);
+ retval = t1[0] + maxOffset < getWidth();
+ }
+ return retval;
+ }
+
+ protected void scrollX(int deltaX) {
+ scrollHandler().scrollBy(deltaX, 0);
+ }
+
+ protected int getScrollOffset() {
+ return scrollHandler().getScrollX();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mZoomRingController.setVisible(false);
+ mZoomButtonsController.setVisible(false);
+ }
+
+ }
+
+ static class ScrollHandler extends LinearLayout {
+ private Runnable mFirstLayoutCompletedCallback = null;
+ private Scroller mScrollerHelper;
+ private int mWidth = -1;
+
+ public ScrollHandler(Context context) {
+ super(context);
+ mScrollerHelper = new Scroller(context);
+ }
+
+ public ScrollHandler(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mScrollerHelper = new Scroller(context);
+ }
+
+ public void setLayoutCompletedCallback(Runnable r) {
+ mFirstLayoutCompletedCallback = r;
+ }
+
+ public void startScrollTo(int newX, int newY) {
+ int oldX = getScrollX();
+ int oldY = getScrollY();
+
+ int deltaX = newX - oldX;
+ int deltaY = newY - oldY;
+
+ if (mWidth == -1) {
+ mWidth = findViewById(R.id.image2).getWidth();
+ }
+ int viewWidth = mWidth;
+
+ int duration = viewWidth > 0
+ ? sBaseScrollDuration * Math.abs(deltaX) / viewWidth
+ : 0;
+ mScrollerHelper.startScroll(oldX, oldY, deltaX, deltaY, duration);
+ invalidate();
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScrollerHelper.computeScrollOffset()) {
+ scrollTo(mScrollerHelper.getCurrX(), mScrollerHelper.getCurrY());
+ postInvalidate(); // So we draw again
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int width = right - left;
+ int x = 0;
+ for (View v : new View[] {
+ findViewById(R.id.image1),
+ findViewById(R.id.image2),
+ findViewById(R.id.image3) }) {
+ v.layout(x, 0, x + width, bottom);
+ x += (width + sPadding);
+ }
+
+ findViewById(R.id.padding1).layout(width, 0, width + sPadding, bottom);
+ findViewById(R.id.padding2).layout(width+sPadding+width, 0, width+sPadding+width+sPadding, bottom);
+
+ if (changed) {
+ if (mFirstLayoutCompletedCallback != null) {
+ mFirstLayoutCompletedCallback.run();
+ }
+ }
+ }
+ }
+
+ private void animateScrollTo(int xNew, int yNew) {
+ mScroller.startScrollTo(xNew, yNew);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ super.onCreateOptionsMenu(menu);
+
+ if (! mCameraReviewMode) {
+ MenuItem item = menu.add(Menu.CATEGORY_SECONDARY, 203, 0, R.string.slide_show);
+ item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ setMode(MODE_SLIDESHOW);
+ mLastSlideShowImage = mCurrentPosition;
+ loadNextImage(mCurrentPosition, 0, true);
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_slideshow);
+ }
+
+ final SelectedImageGetter selectedImageGetter = new SelectedImageGetter() {
+ public ImageManager.IImage getCurrentImage() {
+ return mAllImages.getImageAt(mCurrentPosition);
+ }
+
+ public Uri getCurrentImageUri() {
+ return mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri();
+ }
+ };
+
+ mImageMenuRunnable = MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL,
+ true,
+ ViewImage.this,
+ mHandler,
+ mDeletePhotoRunnable,
+ new MenuHelper.MenuInvoker() {
+ public void run(MenuHelper.MenuCallback cb) {
+ setMode(MODE_NORMAL);
+ cb.run(selectedImageGetter.getCurrentImageUri(), selectedImageGetter.getCurrentImage());
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.recycleBitmaps();
+ iv.setImageBitmap(null, true);
+ }
+ setImage(mCurrentPosition);
+ }
+ });
+
+ if (true) {
+ MenuItem item = menu.add(Menu.CATEGORY_SECONDARY, 203, 1000, R.string.camerasettings);
+ item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent preferences = new Intent();
+ preferences.setClass(ViewImage.this, GallerySettings.class);
+ startActivity(preferences);
+ return true;
+ }
+ });
+ item.setAlphabeticShortcut('p');
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+ }
+
+ // Hidden menu just so the shortcut will bring up the zoom controls
+ menu.add(Menu.CATEGORY_SECONDARY, 203, 0, R.string.camerasettings) // the string resource is a placeholder
+ .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ showOnScreenControls();
+ return true;
+ }
+ })
+ .setAlphabeticShortcut('z')
+ .setVisible(false);
+
+
+ return true;
+ }
+
+ protected Runnable mDeletePhotoRunnable = new Runnable() {
+ public void run() {
+ mAllImages.removeImageAt(mCurrentPosition);
+ if (mAllImages.getCount() == 0) {
+ finish();
+ } else {
+ if (mCurrentPosition == mAllImages.getCount()) {
+ mCurrentPosition -= 1;
+ }
+ }
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.setImageBitmapResetBase(null, true, true);
+ }
+ setImage(mCurrentPosition);
+ }
+ };
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu)
+ {
+ super.onPrepareOptionsMenu(menu);
+ setMode(MODE_NORMAL);
+
+ if (mImageMenuRunnable != null) {
+ mImageMenuRunnable.gettingReadyToOpen(menu, mAllImages.getImageAt(mCurrentPosition));
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onConfigurationChanged(android.content.res.Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ boolean changed = mCurrentOrientation != newConfig.orientation;
+ mCurrentOrientation = newConfig.orientation;
+ if (changed) {
+ if (mGetter != null) {
+ // kill off any background image fetching
+ mGetter.cancelCurrent();
+ mGetter.stop();
+ }
+ makeGetter();
+ mFirst = true;
+
+ // clear off the current set of images since we need to reload
+ // them at the right size
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.clear();
+ }
+ MenuHelper.requestOrientation(this, mPrefs);
+ }
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ boolean b = super.onMenuItemSelected(featureId, item);
+ if (mImageMenuRunnable != null)
+ mImageMenuRunnable.aboutToCall(item, mAllImages.getImageAt(mCurrentPosition));
+ return b;
+ }
+
+ /*
+ * Here's the loading strategy. For any given image, load the thumbnail
+ * into memory and post a callback to display the resulting bitmap.
+ *
+ * Then proceed to load the full image bitmap. Three things can
+ * happen at this point:
+ *
+ * 1. the image fails to load because the UI thread decided
+ * to move on to a different image. This "cancellation" happens
+ * by virtue of the UI thread closing the stream containing the
+ * image being decoded. BitmapFactory.decodeStream returns null
+ * in this case.
+ *
+ * 2. the image loaded successfully. At that point we post
+ * a callback to the UI thread to actually show the bitmap.
+ *
+ * 3. when the post runs it checks to see if the image that was
+ * loaded is still the one we want. The UI may have moved on
+ * to some other image and if so we just drop the newly loaded
+ * bitmap on the floor.
+ */
+
+ interface ImageGetterCallback {
+ public void imageLoaded(int pos, int offset, Bitmap bitmap, boolean isThumb);
+ public boolean wantsThumbnail(int pos, int offset);
+ public boolean wantsFullImage(int pos, int offset);
+ public int fullImageSizeToUse(int pos, int offset);
+ public void completed(boolean wasCanceled);
+ public int [] loadOrder();
+ }
+
+ class ImageGetter {
+ // The thread which does the work.
+ private Thread mGetterThread;
+
+ // The base position that's being retrieved. The actual images retrieved
+ // are this base plus each of the offets.
+ private int mCurrentPosition = -1;
+
+ // The callback to invoke for each image.
+ private ImageGetterCallback mCB;
+
+ // This is the loader cancelable that gets set while we're loading an image.
+ // If we change position we can cancel the current load using this.
+ private ImageManager.IGetBitmap_cancelable mLoad;
+
+ // True if we're canceling the current load.
+ private boolean mCancelCurrent = false;
+
+ // True when the therad should exit.
+ private boolean mDone = false;
+
+ // True when the loader thread is waiting for work.
+ private boolean mReady = false;
+
+ private void cancelCurrent() {
+ synchronized (this) {
+ if (!mReady) {
+ mCancelCurrent = true;
+ ImageManager.IGetBitmap_cancelable load = mLoad;
+ if (load != null) {
+ if (Config.LOGV)
+ Log.v(TAG, "canceling load object");
+ load.cancel();
+ }
+ mCancelCurrent = false;
+ }
+ }
+ }
+
+ public ImageGetter() {
+ mGetterThread = new Thread(new Runnable() {
+
+ private Runnable callback(final int position, final int offset, final boolean isThumb, final Bitmap bitmap) {
+ return new Runnable() {
+ public void run() {
+ // check for inflight callbacks that aren't applicable any longer
+ // before delivering them
+ if (!isCanceled() && position == mCurrentPosition) {
+ mCB.imageLoaded(position, offset, bitmap, isThumb);
+ } else {
+ if (bitmap != null)
+ bitmap.recycle();
+ }
+ }
+ };
+ }
+
+ private Runnable completedCallback(final boolean wasCanceled) {
+ return new Runnable() {
+ public void run() {
+ mCB.completed(wasCanceled);
+ }
+ };
+ }
+
+ public void run() {
+ int lastPosition = -1;
+ while (!mDone) {
+ synchronized (ImageGetter.this) {
+ mReady = true;
+ ImageGetter.this.notify();
+
+ if (mCurrentPosition == -1 || lastPosition == mCurrentPosition) {
+ try {
+ ImageGetter.this.wait();
+ } catch (InterruptedException ex) {
+ continue;
+ }
+ }
+
+ lastPosition = mCurrentPosition;
+ mReady = false;
+ }
+
+ if (lastPosition != -1) {
+ int imageCount = mAllImages.getCount();
+
+ int [] order = mCB.loadOrder();
+ for (int i = 0; i < order.length; i++) {
+ int offset = order[i];
+ int imageNumber = lastPosition + offset;
+ if (imageNumber >= 0 && imageNumber < imageCount) {
+ ImageManager.IImage image = mAllImages.getImageAt(lastPosition + offset);
+ if (image == null || isCanceled()) {
+ break;
+ }
+ if (mCB.wantsThumbnail(lastPosition, offset)) {
+ if (Config.LOGV)
+ Log.v(TAG, "starting THUMBNAIL load at offset " + offset);
+ Bitmap b = image.thumbBitmap();
+ mHandler.post(callback(lastPosition, offset, true, b));
+ }
+ }
+ }
+
+ for (int i = 0; i < order.length; i++) {
+ int offset = order[i];
+ int imageNumber = lastPosition + offset;
+ if (imageNumber >= 0 && imageNumber < imageCount) {
+ ImageManager.IImage image = mAllImages.getImageAt(lastPosition + offset);
+ if (mCB.wantsFullImage(lastPosition, offset)) {
+ if (Config.LOGV)
+ Log.v(TAG, "starting FULL IMAGE load at offset " + offset);
+ int sizeToUse = mCB.fullImageSizeToUse(lastPosition, offset);
+ if (image != null && !isCanceled()) {
+ mLoad = image.fullSizeBitmap_cancelable(sizeToUse);
+ }
+ if (mLoad != null) {
+ long t1;
+ if (Config.LOGV) t1 = System.currentTimeMillis();
+
+ Bitmap b = null;
+ try {
+ b = mLoad.get();
+ } catch (OutOfMemoryError e) {
+ Log.e(TAG, "couldn't load full size bitmap for " + "");
+ }
+ if (Config.LOGV && b != null) {
+ long t2 = System.currentTimeMillis();
+ Log.v(TAG, "loading full image for " + image.fullSizeImageUri()
+ + " with requested size " + sizeToUse
+ + " took " + (t2-t1)
+ + " and returned a bitmap with size "
+ + b.getWidth() + " / " + b.getHeight());
+ }
+
+ mLoad = null;
+ if (b != null) {
+ if (isCanceled()) {
+ b.recycle();
+ } else {
+ mHandler.post(callback(lastPosition, offset, false, b));
+ }
+ }
+ }
+ }
+ }
+ }
+ mHandler.post(completedCallback(isCanceled()));
+ }
+ }
+ }
+ });
+ mGetterThread.setName("ImageGettter");
+ mGetterThread.start();
+ }
+
+ private boolean isCanceled() {
+ synchronized (this) {
+ return mCancelCurrent;
+ }
+ }
+
+ public void setPosition(int position, ImageGetterCallback cb) {
+ synchronized (this) {
+ if (!mReady) {
+ try {
+ mCancelCurrent = true;
+ ImageManager.IGetBitmap_cancelable load = mLoad;
+ if (load != null) {
+ load.cancel();
+ }
+ // if the thread is waiting before loading the full size
+ // image then this will free it up
+ ImageGetter.this.notify();
+ ImageGetter.this.wait();
+ mCancelCurrent = false;
+ } catch (InterruptedException ex) {
+ // not sure what to do here
+ }
+ }
+ }
+
+ mCurrentPosition = position;
+ mCB = cb;
+
+ synchronized (this) {
+ ImageGetter.this.notify();
+ }
+ }
+
+ public void stop() {
+ synchronized (this) {
+ mDone = true;
+ ImageGetter.this.notify();
+ }
+ try {
+ mGetterThread.join();
+ } catch (InterruptedException ex) {
+
+ }
+ }
+ }
+
+ private void setImage(int pos) {
+ if (!mLayoutComplete) {
+ return;
+ }
+
+ final boolean left = mCurrentPosition > pos;
+
+ mCurrentPosition = pos;
+
+ ImageViewTouchBase current = mImageViews[1];
+ current.mSuppMatrix.reset();
+ current.setImageMatrix(current.getImageViewMatrix());
+
+ if (false) {
+ Log.v(TAG, "before...");
+ for (ImageViewTouchBase ivtb : mImageViews)
+ ivtb.dump();
+ }
+
+ if (!mFirst) {
+ if (left) {
+ mImageViews[2].copyFrom(mImageViews[1]);
+ mImageViews[1].copyFrom(mImageViews[0]);
+ } else {
+ mImageViews[0].copyFrom(mImageViews[1]);
+ mImageViews[1].copyFrom(mImageViews[2]);
+ }
+ }
+ if (false) {
+ Log.v(TAG, "after copy...");
+ for (ImageViewTouchBase ivtb : mImageViews)
+ ivtb.dump();
+ }
+
+ for (ImageViewTouchBase ivt: mImageViews) {
+ ivt.mIsZooming = false;
+ }
+ int width = mImageViews[1].getWidth();
+ int from;
+ int to = width + sPadding;
+ if (mFirst) {
+ from = to;
+ mFirst = false;
+ } else {
+ from = left ? (width + sPadding) + mScroller.getScrollX()
+ : mScroller.getScrollX() - (width + sPadding);
+ }
+
+ if (sAnimateTransitions) {
+ mScroller.scrollTo(from, 0);
+ animateScrollTo(to, 0);
+ } else {
+ mScroller.scrollTo(to, 0);
+ }
+
+ ImageGetterCallback cb = new ImageGetterCallback() {
+ public void completed(boolean wasCanceled) {
+ if (!mShowActionIcons) {
+ mImageViews[1].setFocusableInTouchMode(true);
+ mImageViews[1].requestFocus();
+ }
+ }
+
+ public boolean wantsThumbnail(int pos, int offset) {
+ ImageViewTouchBase ivt = mImageViews[1 + offset];
+ return ivt.mThumbBitmap == null;
+ }
+
+ public boolean wantsFullImage(int pos, int offset) {
+ ImageViewTouchBase ivt = mImageViews[1 + offset];
+ if (ivt.mBitmapDisplayed != null && !ivt.mBitmapIsThumbnail) {
+ return false;
+ }
+ if (offset != 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public int fullImageSizeToUse(int pos, int offset) {
+ // TODO
+ // this number should be bigger so that we can zoom. we may need to
+ // get fancier and read in the fuller size image as the user starts
+ // to zoom. use -1 to get the full full size image.
+ // for now use 480 so we don't run out of memory
+ final int imageViewSize = 480;
+ return imageViewSize;
+ }
+
+ public int [] loadOrder() {
+ return sOrder_adjacents;
+ }
+
+ public void imageLoaded(int pos, int offset, Bitmap bitmap, boolean isThumb) {
+ ImageViewTouchBase ivt = mImageViews[1 + offset];
+ ivt.setImageBitmapResetBase(bitmap, isThumb, isThumb);
+ }
+ };
+
+ // Could be null if we're stopping a slide show in the course of pausing
+ if (mGetter != null) {
+ mGetter.setPosition(pos, cb);
+ }
+ showOnScreenControls();
+ }
+
+ @Override
+ public void onCreate(Bundle instanceState)
+ {
+ super.onCreate(instanceState);
+ Intent intent = getIntent();
+ mCameraReviewMode = intent.getBooleanExtra("com.android.camera.ReviewMode", false);
+ mFullScreenInNormalMode = intent.getBooleanExtra(MediaStore.EXTRA_FULL_SCREEN, true);
+ mShowActionIcons = intent.getBooleanExtra(MediaStore.EXTRA_SHOW_ACTION_ICONS, false);
+
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mCurrentOrientation = getResources().getConfiguration().orientation;
+
+ setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.viewimage);
+
+ mImageViews[0] = (ImageViewTouch) findViewById(R.id.image1);
+ mImageViews[1] = (ImageViewTouch) findViewById(R.id.image2);
+ mImageViews[2] = (ImageViewTouch) findViewById(R.id.image3);
+
+ for(ImageViewTouch v : mImageViews) {
+ v.setEnableTrackballScroll(!mShowActionIcons);
+ }
+
+ mScroller = (ScrollHandler)findViewById(R.id.scroller);
+ makeGetter();
+
+ mAnimationIndex = -1;
+
+ mSlideShowInAnimation = new Animation[] {
+ makeInAnimation(R.anim.transition_in),
+ makeInAnimation(R.anim.slide_in),
+ makeInAnimation(R.anim.slide_in_vertical),
+ };
+
+ mSlideShowOutAnimation = new Animation[] {
+ makeOutAnimation(R.anim.transition_out),
+ makeOutAnimation(R.anim.slide_out),
+ makeOutAnimation(R.anim.slide_out_vertical),
+ };
+
+ mSlideShowImageViews[0] = (ImageViewTouch) findViewById(R.id.image1_slideShow);
+ mSlideShowImageViews[1] = (ImageViewTouch) findViewById(R.id.image2_slideShow);
+ for (ImageViewTouch v : mSlideShowImageViews) {
+ v.setImageBitmapResetBase(null, true, true);
+ v.setVisibility(View.INVISIBLE);
+ v.setEnableTrackballScroll(!mShowActionIcons);
+ }
+
+ mActionIconPanel = findViewById(R.id.action_icon_panel);
+ {
+ int[] pickIds = {R.id.attach, R.id.cancel};
+ int[] normalIds = {R.id.gallery, R.id.setas, R.id.share, R.id.discard};
+ int[] hideIds = pickIds;
+ int[] connectIds = normalIds;
+ if (isPickIntent()) {
+ hideIds = normalIds;
+ connectIds = pickIds;
+ }
+ for(int id : hideIds) {
+ mActionIconPanel.findViewById(id).setVisibility(View.GONE);
+ }
+ for(int id : connectIds) {
+ View view = mActionIconPanel.findViewById(id);
+ view.setOnClickListener(this);
+ Animation animation = new AlphaAnimation(0F, 1F);
+ animation.setDuration(500);
+ view.setAnimation(animation);
+ }
+ }
+ mShutterButton = findViewById(R.id.shutter_button);
+ mShutterButton.setOnClickListener(this);
+
+ Uri uri = getIntent().getData();
+
+ if (Config.LOGV)
+ Log.v(TAG, "uri is " + uri);
+ if (instanceState != null) {
+ if (instanceState.containsKey("uri")) {
+ uri = Uri.parse(instanceState.getString("uri"));
+ }
+ }
+ if (uri == null) {
+ finish();
+ return;
+ }
+ init(uri);
+
+ Bundle b = getIntent().getExtras();
+
+ boolean slideShow = b != null ? b.getBoolean("slideshow", false) : false;
+ if (slideShow) {
+ setMode(MODE_SLIDESHOW);
+ loadNextImage(mCurrentPosition, 0, true);
+ } else {
+ if (mFullScreenInNormalMode) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ if (mShowActionIcons) {
+ mActionIconPanel.setVisibility(View.VISIBLE);
+ mShutterButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ setupDismissOnScreenControlRunnable();
+
+ mNextImageView = findViewById(R.id.next_image);
+ mPrevImageView = findViewById(R.id.prev_image);
+ mNextImageView.setOnClickListener(this);
+ mPrevImageView.setOnClickListener(this);
+
+ if (mShowActionIcons) {
+ mNextImageView.setFocusable(true);
+ mPrevImageView.setFocusable(true);
+ }
+
+ setOrientation();
+ }
+
+ private void setOrientation() {
+ Intent intent = getIntent();
+ if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) {
+ int orientation = intent.getIntExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ if (orientation != getRequestedOrientation()) {
+ setRequestedOrientation(orientation);
+ }
+ } else {
+ MenuHelper.requestOrientation(this, mPrefs);
+ }
+ }
+
+ private Animation makeInAnimation(int id) {
+ Animation inAnimation = AnimationUtils.loadAnimation(this, id);
+ return inAnimation;
+ }
+
+ private Animation makeOutAnimation(int id) {
+ Animation outAnimation = AnimationUtils.loadAnimation(this, id);
+ return outAnimation;
+ }
+
+ private void setMode(int mode) {
+ if (mMode == mode) {
+ return;
+ }
+
+ findViewById(R.id.slideShowContainer).setVisibility(mode == MODE_SLIDESHOW ? View.VISIBLE : View.GONE);
+ findViewById(R.id.abs) .setVisibility(mode == MODE_NORMAL ? View.VISIBLE : View.GONE);
+
+ Window win = getWindow();
+ mMode = mode;
+ if (mode == MODE_SLIDESHOW) {
+ win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ if (sSlideShowHidesStatusBar) {
+ win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ for (ImageViewTouchBase ivt: mImageViews) {
+ ivt.clear();
+ }
+ mActionIconPanel.setVisibility(View.GONE);
+ mShutterButton.setVisibility(View.GONE);
+
+ if (false) {
+ Log.v(TAG, "current is " + this.mSlideShowImageCurrent);
+ this.mSlideShowImageViews[0].dump();
+ this.mSlideShowImageViews[0].dump();
+ }
+
+ findViewById(R.id.slideShowContainer).getRootView().requestLayout();
+ mUseShuffleOrder = mPrefs.getBoolean("pref_gallery_slideshow_shuffle_key", false);
+ mSlideShowLoop = mPrefs.getBoolean("pref_gallery_slideshow_repeat_key", false);
+ try {
+ mAnimationIndex = Integer.parseInt(mPrefs.getString("pref_gallery_slideshow_transition_key", "0"));
+ } catch (Exception ex) {
+ Log.e(TAG, "couldn't parse preference: " + ex.toString());
+ mAnimationIndex = 0;
+ }
+ try {
+ mSlideShowInterval = Integer.parseInt(mPrefs.getString("pref_gallery_slideshow_interval_key", "3")) * 1000;
+ } catch (Exception ex) {
+ Log.e(TAG, "couldn't parse preference: " + ex.toString());
+ mSlideShowInterval = 3000;
+ }
+
+ if (Config.LOGV) {
+ Log.v(TAG, "read prefs... shuffle: " + mUseShuffleOrder);
+ Log.v(TAG, "read prefs... loop: " + mSlideShowLoop);
+ Log.v(TAG, "read prefs... animidx: " + mAnimationIndex);
+ Log.v(TAG, "read prefs... interval: " + mSlideShowInterval);
+ }
+
+ if (mUseShuffleOrder) {
+ generateShuffleOrder();
+ }
+ } else {
+ if (Config.LOGV)
+ Log.v(TAG, "slide show mode off, mCurrentPosition == " + mCurrentPosition);
+ win.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ if (mFullScreenInNormalMode) {
+ win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ } else {
+ win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+
+ if (mGetter != null)
+ mGetter.cancelCurrent();
+
+ if (sSlideShowHidesStatusBar) {
+ win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ if (mShowActionIcons) {
+ mActionIconPanel.setVisibility(View.VISIBLE);
+ mShutterButton.setVisibility(View.VISIBLE);
+ }
+
+ ImageViewTouchBase dst = mImageViews[1];
+ dst.mLastXTouchPos = -1;
+ dst.mLastYTouchPos = -1;
+
+ for (ImageViewTouchBase ivt: mSlideShowImageViews) {
+ ivt.clear();
+ }
+
+ mShuffleOrder = null;
+
+ // mGetter null is a proxy for being paused
+ if (mGetter != null) {
+ mFirst = true; // don't animate
+ setImage(mCurrentPosition);
+ }
+ }
+
+ // this line shouldn't be necessary but the view hierarchy doesn't
+ // seem to realize that the window layout changed
+ mScroller.requestLayout();
+ }
+
+ private void generateShuffleOrder() {
+ if (mShuffleOrder == null || mShuffleOrder.length != mAllImages.getCount()) {
+ mShuffleOrder = new int[mAllImages.getCount()];
+ }
+
+ for (int i = 0; i < mShuffleOrder.length; i++) {
+ mShuffleOrder[i] = i;
+ }
+
+ for (int i = mShuffleOrder.length - 1; i > 0; i--) {
+ int r = mRandom.nextInt(i);
+ int tmp = mShuffleOrder[r];
+ mShuffleOrder[r] = mShuffleOrder[i];
+ mShuffleOrder[i] = tmp;
+ }
+ }
+
+ private void loadNextImage(final int requestedPos, final long delay, final boolean firstCall) {
+ if (firstCall && mUseShuffleOrder) {
+ generateShuffleOrder();
+ }
+
+ final long targetDisplayTime = System.currentTimeMillis() + delay;
+
+ ImageGetterCallback cb = new ImageGetterCallback() {
+ public void completed(boolean wasCanceled) {
+ }
+
+ public boolean wantsThumbnail(int pos, int offset) {
+ return true;
+ }
+
+ public boolean wantsFullImage(int pos, int offset) {
+ return false;
+ }
+
+ public int [] loadOrder() {
+ return sOrder_slideshow;
+ }
+
+ public int fullImageSizeToUse(int pos, int offset) {
+ return 480; // TODO compute this
+ }
+
+ public void imageLoaded(final int pos, final int offset, final Bitmap bitmap, final boolean isThumb) {
+ long timeRemaining = Math.max(0, targetDisplayTime - System.currentTimeMillis());
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ if (mMode == MODE_NORMAL) {
+ return;
+ }
+
+ ImageViewTouchBase oldView = mSlideShowImageViews[mSlideShowImageCurrent];
+
+ if (++mSlideShowImageCurrent == mSlideShowImageViews.length) {
+ mSlideShowImageCurrent = 0;
+ }
+
+ ImageViewTouchBase newView = mSlideShowImageViews[mSlideShowImageCurrent];
+ newView.setVisibility(View.VISIBLE);
+ newView.setImageBitmapResetBase(bitmap, isThumb, isThumb);
+ newView.bringToFront();
+
+ int animation = 0;
+
+ if (mAnimationIndex == -1) {
+ int n = mRandom.nextInt(mSlideShowInAnimation.length);
+ animation = n;
+ } else {
+ animation = mAnimationIndex;
+ }
+
+ Animation aIn = mSlideShowInAnimation[animation];
+ newView.setAnimation(aIn);
+ newView.setVisibility(View.VISIBLE);
+ aIn.startNow();
+
+ Animation aOut = mSlideShowOutAnimation[animation];
+ oldView.setVisibility(View.INVISIBLE);
+ oldView.setAnimation(aOut);
+ aOut.startNow();
+
+ mCurrentPosition = requestedPos;
+
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (mCurrentPosition == mLastSlideShowImage && !firstCall) {
+ if (mSlideShowLoop) {
+ if (mUseShuffleOrder) {
+ generateShuffleOrder();
+ }
+ } else {
+ setMode(MODE_NORMAL);
+ return;
+ }
+ }
+
+ if (Config.LOGV)
+ Log.v(TAG, "mCurrentPosition is now " + mCurrentPosition);
+ loadNextImage((mCurrentPosition + 1) % mAllImages.getCount(), mSlideShowInterval, false);
+ }
+ });
+ }
+ }, timeRemaining);
+ }
+ };
+ // Could be null if we're stopping a slide show in the course of pausing
+ if (mGetter != null) {
+ int pos = requestedPos;
+ if (mShuffleOrder != null) {
+ pos = mShuffleOrder[pos];
+ }
+ mGetter.setPosition(pos, cb);
+ }
+ }
+
+ private void makeGetter() {
+ mGetter = new ImageGetter();
+ }
+
+ private boolean desiredSortOrder() {
+ String sortOrder = mPrefs.getString("pref_gallery_sort_key", null);
+ boolean sortAscending = false;
+ if (sortOrder != null) {
+ sortAscending = sortOrder.equals("ascending");
+ }
+ if (mCameraReviewMode) {
+ // Force left-arrow older pictures, right-arrow newer pictures.
+ sortAscending = true;
+ }
+ return sortAscending;
+ }
+
+ private void init(Uri uri) {
+ mSortAscending = desiredSortOrder();
+ int sort = mSortAscending ? ImageManager.SORT_ASCENDING : ImageManager.SORT_DESCENDING;
+ mAllImages = ImageManager.makeImageList(uri, this, sort);
+
+ uri = uri.buildUpon().query(null).build();
+ // TODO smarter/faster here please
+ for (int i = 0; i < mAllImages.getCount(); i++) {
+ ImageManager.IImage image = mAllImages.getImageAt(i);
+ if (image.fullSizeImageUri().equals(uri)) {
+ mCurrentPosition = i;
+ mLastSlideShowImage = mCurrentPosition;
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle b) {
+ super.onSaveInstanceState(b);
+ ImageManager.IImage image = mAllImages.getImageAt(mCurrentPosition);
+
+ if (image != null){
+ Uri uri = image.fullSizeImageUri();
+ String bucket = null;
+ if(getIntent()!= null && getIntent().getData()!=null)
+ bucket = getIntent().getData().getQueryParameter("bucketId");
+
+ if(bucket!=null)
+ uri = uri.buildUpon().appendQueryParameter("bucketId", bucket).build();
+
+ b.putString("uri", uri.toString());
+ }
+ if (mMode == MODE_SLIDESHOW)
+ b.putBoolean("slideshow", true);
+ }
+
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+
+ // normally this will never be zero but if one "backs" into this
+ // activity after removing the sdcard it could be zero. in that
+ // case just "finish" since there's nothing useful that can happen.
+ if (mAllImages.getCount() == 0) {
+ finish();
+ }
+
+ ImageManager.IImage image = mAllImages.getImageAt(mCurrentPosition);
+
+ if (desiredSortOrder() != mSortAscending) {
+ init(image.fullSizeImageUri());
+ }
+
+ if (mGetter == null) {
+ makeGetter();
+ }
+
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.setImageBitmap(null, true);
+ }
+
+ mFirst = true;
+ mScroller.setLayoutCompletedCallback(new Runnable() {
+ public void run() {
+ mLayoutComplete = true;
+ setImage(mCurrentPosition);
+ }
+ });
+ setImage(mCurrentPosition);
+
+ setOrientation();
+
+ // Show a tutorial for the new zoom interaction (the method ensure we only show it once)
+ ZoomRingController.showZoomTutorialOnce(this);
+ }
+
+ @Override
+ public void onPause()
+ {
+ super.onPause();
+
+ mGetter.cancelCurrent();
+ mGetter.stop();
+ mGetter = null;
+ setMode(MODE_NORMAL);
+
+ mAllImages.deactivate();
+
+ for (ImageViewTouch iv: mImageViews) {
+ iv.recycleBitmaps();
+ iv.setImageBitmap(null, true);
+ }
+
+ for (ImageViewTouch iv: mSlideShowImageViews) {
+ iv.recycleBitmaps();
+ iv.setImageBitmap(null, true);
+ }
+ ZoomRingController.finishZoomTutorial(this, false);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ }
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+
+ case R.id.shutter_button: {
+ if (mCameraReviewMode) {
+ finish();
+ } else {
+ MenuHelper.gotoStillImageCapture(this);
+ }
+ }
+ break;
+
+ case R.id.gallery: {
+ MenuHelper.gotoCameraImageGallery(this);
+ }
+ break;
+
+ case R.id.discard: {
+ if (mCameraReviewMode) {
+ mDeletePhotoRunnable.run();
+ } else {
+ MenuHelper.deletePhoto(this, mDeletePhotoRunnable);
+ }
+ }
+ break;
+
+ case R.id.share: {
+ Uri u = mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri();
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType("image/jpeg");
+ intent.putExtra(Intent.EXTRA_STREAM, u);
+ try {
+ startActivity(Intent.createChooser(intent, getText(R.string.sendImage)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(this, R.string.no_way_to_share_image, Toast.LENGTH_SHORT).show();
+ }
+ }
+ break;
+
+ case R.id.setas: {
+ Uri u = mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri();
+ Intent intent = new Intent(Intent.ACTION_ATTACH_DATA, u);
+ try {
+ startActivity(Intent.createChooser(intent, getText(R.string.setImage)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(this, R.string.no_way_to_share_video, Toast.LENGTH_SHORT).show();
+ }
+ }
+ break;
+
+ case R.id.next_image: {
+ moveNextOrPrevious(1);
+ }
+ break;
+
+ case R.id.prev_image: {
+ moveNextOrPrevious(-1);
+ }
+ break;
+ }
+ }
+
+ private void moveNextOrPrevious(int delta) {
+ int nextImagePos = mCurrentPosition + delta;
+ if ((0 <= nextImagePos) && (nextImagePos < mAllImages.getCount())) {
+ setImage(nextImagePos);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case MenuHelper.RESULT_COMMON_MENU_CROP:
+ if (resultCode == RESULT_OK) {
+ // The CropImage activity passes back the Uri of the cropped image as
+ // the Action rather than the Data.
+ Uri dataUri = Uri.parse(data.getAction());
+ init(dataUri);
+ }
+ break;
+ }
+ }
+}
diff --git a/src/com/android/camera/Wallpaper.java b/src/com/android/camera/Wallpaper.java
new file mode 100644
index 0000000..5c7d0e5
--- /dev/null
+++ b/src/com/android/camera/Wallpaper.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2007 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;
+
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+/**
+ * Wallpaper picker for the camera application. This just redirects to the standard pick action.
+ */
+public class Wallpaper extends Activity {
+ private static final String LOG_TAG = "Camera";
+ static final int PHOTO_PICKED = 1;
+ static final int CROP_DONE = 2;
+
+ static final int SHOW_PROGRESS = 0;
+ static final int FINISH = 1;
+
+ static final String sDoLaunchIcicle = "do_launch";
+ static final String sTempFilePathIcicle = "temp_file_path";
+
+ private ProgressDialog mProgressDialog = null;
+ private boolean mDoLaunch = true;
+ private String mTempFilePath;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SHOW_PROGRESS: {
+ CharSequence c = getText(R.string.wallpaper);
+ mProgressDialog = ProgressDialog.show(Wallpaper.this, "", c, true, false);
+ break;
+ }
+
+ case FINISH: {
+ closeProgressDialog();
+ setResult(RESULT_OK);
+ finish();
+ break;
+ }
+ }
+ }
+ };
+
+ static class SetWallpaperThread extends Thread {
+ private final Bitmap mBitmap;
+ private final Handler mHandler;
+ private final Context mContext;
+ private final File mFile;
+
+ public SetWallpaperThread(Bitmap bitmap, Handler handler, Context context, File file) {
+ mBitmap = bitmap;
+ mHandler = handler;
+ mContext = context;
+ mFile = file;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mContext.setWallpaper(mBitmap);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Failed to set wallpaper.", e);
+ } finally {
+ mHandler.sendEmptyMessage(FINISH);
+ mFile.delete();
+ }
+ }
+ }
+
+ private synchronized void closeProgressDialog() {
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss();
+ mProgressDialog = null;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ if (icicle != null) {
+ mDoLaunch = icicle.getBoolean(sDoLaunchIcicle);
+ mTempFilePath = icicle.getString(sTempFilePathIcicle);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle icicle) {
+ icicle.putBoolean(sDoLaunchIcicle, mDoLaunch);
+ icicle.putString(sTempFilePathIcicle, mTempFilePath);
+ }
+
+ @Override
+ protected void onPause() {
+ closeProgressDialog();
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (!mDoLaunch) {
+ return;
+ }
+ Uri imageToUse = getIntent().getData();
+ if (imageToUse != null) {
+ Intent intent = new Intent();
+ intent.setClassName("com.android.camera", "com.android.camera.CropImage");
+ intent.setData(imageToUse);
+ formatIntent(intent);
+ startActivityForResult(intent, CROP_DONE);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+ intent.setType("image/*");
+ intent.putExtra("crop", "true");
+ formatIntent(intent);
+ startActivityForResult(intent, PHOTO_PICKED);
+ }
+ }
+
+ protected void formatIntent(Intent intent) {
+ // TODO: A temporary file is NOT necessary
+ // The CropImage intent should be able to set the wallpaper directly
+ // without writing to a file, which we then need to read here to write
+ // it again as the final wallpaper, this is silly
+ File f = getFileStreamPath("temp-wallpaper");
+ (new File(f.getParent())).mkdirs();
+ mTempFilePath = f.toString();
+ f.delete();
+
+ int width = getWallpaperDesiredMinimumWidth();
+ int height = getWallpaperDesiredMinimumHeight();
+ intent.putExtra("outputX", width);
+ intent.putExtra("outputY", height);
+ intent.putExtra("aspectX", width);
+ intent.putExtra("aspectY", height);
+ intent.putExtra("scale", true);
+ intent.putExtra("noFaceDetection", true);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.parse("file:/" + mTempFilePath));
+ intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.name());
+ // TODO: we should have an extra called "setWallpaper" to ask CropImage to
+ // set the cropped image as a wallpaper directly
+ // This means the SetWallpaperThread should be moved out of this class to CropImage
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if ((requestCode == PHOTO_PICKED || requestCode == CROP_DONE) && (resultCode == RESULT_OK)
+ && (data != null)) {
+ try {
+ File tempFile = new File(mTempFilePath);
+ InputStream s = new FileInputStream(tempFile);
+ Bitmap bitmap = BitmapFactory.decodeStream(s);
+ if (bitmap == null) {
+ Log.e(LOG_TAG, "Failed to set wallpaper. Couldn't get bitmap for path " + mTempFilePath);
+ } else {
+ if (android.util.Config.LOGV)
+ Log.v(LOG_TAG, "bitmap size is " + bitmap.getWidth() + " / " + bitmap.getHeight());
+ mHandler.sendEmptyMessage(SHOW_PROGRESS);
+ new SetWallpaperThread(bitmap, mHandler, this, tempFile).start();
+ }
+ mDoLaunch = false;
+ } catch (FileNotFoundException ex) {
+
+ } catch (IOException ex) {
+
+ }
+ } else {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+}