diff options
Diffstat (limited to 'src/com')
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(); + } + } +} |