summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/android/camera/BufferedInputStream.java231
-rw-r--r--src/com/android/camera/Camera.java1527
-rw-r--r--src/com/android/camera/CameraButtonIntentReceiver.java43
-rw-r--r--src/com/android/camera/CameraSettings.java65
-rw-r--r--src/com/android/camera/CameraThread.java95
-rw-r--r--src/com/android/camera/CropImage.java794
-rw-r--r--src/com/android/camera/DrmWallpaper.java35
-rw-r--r--src/com/android/camera/ErrorScreen.java95
-rw-r--r--src/com/android/camera/ExifInterface.java263
-rw-r--r--src/com/android/camera/GalleryPicker.java607
-rw-r--r--src/com/android/camera/GalleryPickerItem.java98
-rw-r--r--src/com/android/camera/GallerySettings.java46
-rw-r--r--src/com/android/camera/HighlightView.java428
-rw-r--r--src/com/android/camera/ImageGallery2.java1734
-rw-r--r--src/com/android/camera/ImageLoader.java348
-rwxr-xr-xsrc/com/android/camera/ImageManager.java3632
-rw-r--r--src/com/android/camera/ImageViewTouchBase.java547
-rw-r--r--src/com/android/camera/MenuHelper.java521
-rw-r--r--src/com/android/camera/PickWallpaper.java23
-rw-r--r--src/com/android/camera/PwaUpload.java56
-rw-r--r--src/com/android/camera/SelectedImageGetter.java25
-rw-r--r--src/com/android/camera/SlideShow.java425
-rw-r--r--src/com/android/camera/UploadAction.java34
-rw-r--r--src/com/android/camera/UploadService.java1181
-rw-r--r--src/com/android/camera/ViewImage.java1434
-rw-r--r--src/com/android/camera/ViewVideo.java210
-rw-r--r--src/com/android/camera/Wallpaper.java193
-rw-r--r--src/com/android/camera/YouTubeUpload.java145
28 files changed, 14835 insertions, 0 deletions
diff --git a/src/com/android/camera/BufferedInputStream.java b/src/com/android/camera/BufferedInputStream.java
new file mode 100644
index 0000000..1505e87
--- /dev/null
+++ b/src/com/android/camera/BufferedInputStream.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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;
+/*
+* This file derives from the Apache version of the BufferedInputStream.
+* Mods to support passing in the buffer rather than creating one directly.
+*/
+import java.io.InputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+
+public class BufferedInputStream extends FilterInputStream {
+ protected byte[] buf;
+ protected int count;
+ protected int marklimit;
+ protected int markpos = -1;
+ protected int pos;
+
+ private boolean closed = false;
+
+ public BufferedInputStream(InputStream in, byte [] buffer) {
+ super(in);
+ buf = buffer;
+ if (buf == null) {
+ throw new java.security.InvalidParameterException();
+ }
+ }
+
+ @Override
+ public synchronized int available() throws IOException {
+ return count - pos + in.available();
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (null != in) {
+ super.close();
+ in = null;
+ }
+ buf = null;
+ closed = true;
+ }
+
+ private int fillbuf() throws IOException {
+ if (markpos == -1 || (pos - markpos >= marklimit)) {
+ /* Mark position not set or exceeded readlimit */
+ int result = in.read(buf);
+ if (result > 0) {
+ markpos = -1;
+ pos = 0;
+ count = result == -1 ? 0 : result;
+ }
+ return result;
+ }
+ if (markpos == 0 && marklimit > buf.length) {
+ /* Increase buffer size to accomodate the readlimit */
+ int newLength = buf.length * 2;
+ if (newLength > marklimit) {
+ newLength = marklimit;
+ }
+ byte[] newbuf = new byte[newLength];
+ System.arraycopy(buf, 0, newbuf, 0, buf.length);
+ buf = newbuf;
+ } else if (markpos > 0) {
+ System.arraycopy(buf, markpos, buf, 0, buf.length - markpos);
+ }
+ /* Set the new position and mark position */
+ pos -= markpos;
+ count = markpos = 0;
+ int bytesread = in.read(buf, pos, buf.length - pos);
+ count = bytesread <= 0 ? pos : pos + bytesread;
+ return bytesread;
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ marklimit = readlimit;
+ markpos = pos;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
+
+ @Override
+ public synchronized int read() throws IOException {
+ if (in == null) {
+ // K0059=Stream is closed
+ throw new IOException(); //$NON-NLS-1$
+ }
+
+ /* Are there buffered bytes available? */
+ if (pos >= count && fillbuf() == -1) {
+ return -1; /* no, fill buffer */
+ }
+
+ /* Did filling the buffer fail with -1 (EOF)? */
+ if (count - pos > 0) {
+ return buf[pos++] & 0xFF;
+ }
+ return -1;
+ }
+
+ @Override
+ public synchronized int read(byte[] buffer, int offset, int length)
+ throws IOException {
+ if (closed) {
+ // K0059=Stream is closed
+ throw new IOException(); //$NON-NLS-1$
+ }
+ // avoid int overflow
+ if (offset > buffer.length - length || offset < 0 || length < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (length == 0) {
+ return 0;
+ }
+ if (null == buf) {
+ throw new IOException(); //$NON-NLS-1$
+ }
+
+ int required;
+ if (pos < count) {
+ /* There are bytes available in the buffer. */
+ int copylength = count - pos >= length ? length : count - pos;
+ System.arraycopy(buf, pos, buffer, offset, copylength);
+ pos += copylength;
+ if (copylength == length || in.available() == 0) {
+ return copylength;
+ }
+ offset += copylength;
+ required = length - copylength;
+ } else {
+ required = length;
+ }
+
+ while (true) {
+ int read;
+ /*
+ * If we're not marked and the required size is greater than the
+ * buffer, simply read the bytes directly bypassing the buffer.
+ */
+ if (markpos == -1 && required >= buf.length) {
+ read = in.read(buffer, offset, required);
+ if (read == -1) {
+ return required == length ? -1 : length - required;
+ }
+ } else {
+ if (fillbuf() == -1) {
+ return required == length ? -1 : length - required;
+ }
+ read = count - pos >= required ? required : count - pos;
+ System.arraycopy(buf, pos, buffer, offset, read);
+ pos += read;
+ }
+ required -= read;
+ if (required == 0) {
+ return length;
+ }
+ if (in.available() == 0) {
+ return length - required;
+ }
+ offset += read;
+ }
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ if (closed) {
+ // K0059=Stream is closed
+ throw new IOException(); //$NON-NLS-1$
+ }
+ if (-1 == markpos) {
+ // K005a=Mark has been invalidated.
+ throw new IOException(); //$NON-NLS-1$
+ }
+ pos = markpos;
+ }
+
+ @Override
+ public synchronized long skip(long amount) throws IOException {
+ if (null == in) {
+ // K0059=Stream is closed
+ throw new IOException(); //$NON-NLS-1$
+ }
+ if (amount < 1) {
+ return 0;
+ }
+
+ if (count - pos >= amount) {
+ pos += amount;
+ return amount;
+ }
+ long read = count - pos;
+ pos = count;
+
+ if (markpos != -1) {
+ if (amount <= marklimit) {
+ if (fillbuf() == -1) {
+ return read;
+ }
+ if (count - pos >= amount - read) {
+ pos += amount - read;
+ return amount;
+ }
+ // Couldn't get all the bytes, skip what we read
+ read += (count - pos);
+ pos = count;
+ return read;
+ }
+ markpos = -1;
+ }
+ return read + in.skip(amount - read);
+ }
+}
diff --git a/src/com/android/camera/Camera.java b/src/com/android/camera/Camera.java
new file mode 100644
index 0000000..ef99842
--- /dev/null
+++ b/src/com/android/camera/Camera.java
@@ -0,0 +1,1527 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+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.ColorDrawable;
+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.StatFs;
+import android.os.SystemClock;
+import android.pim.DateFormat;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.OrientationListener;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public class Camera extends Activity implements View.OnClickListener, SurfaceHolder.Callback {
+
+ private static final String TAG = "camera";
+
+ private static final boolean 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;
+
+ private static final int NO_STORAGE_ERROR = -1;
+ private static final int CANNOT_STAT_ERROR = -2;
+
+ public static final int MENU_SWITCH_TO_VIDEO = 0;
+ public static final int MENU_FLASH_SETTING = 1;
+ public static final int MENU_FLASH_AUTO = 2;
+ public static final int MENU_FLASH_ON = 3;
+ public static final int MENU_FLASH_OFF = 4;
+ public static final int MENU_SETTINGS = 5;
+ public static final int MENU_GALLERY_PHOTOS = 6;
+ public static final int MENU_SAVE_SELECT_PHOTOS = 30;
+ public static final int MENU_SAVE_NEW_PHOTO = 31;
+ public static final int MENU_SAVE_SELECTVIDEO = 32;
+ public static final int MENU_SAVE_TAKE_NEW_VIDEO = 33;
+ 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;
+
+ Toast mToast;
+ OrientationListener mOrientationListener;
+ int mLastOrientation = OrientationListener.ORIENTATION_UNKNOWN;
+ SharedPreferences mPreferences;
+
+ static final int IDLE = 1;
+ static final int SNAPSHOT_IN_PROGRESS = 2;
+ static final int SNAPSHOT_COMPLETED = 3;
+
+ int mStatus = IDLE;
+
+ android.hardware.Camera mCameraDevice;
+ SurfaceView mSurfaceView;
+ SurfaceHolder mSurfaceHolder = null;
+ ImageView mBlackout = null;
+
+ int mViewFinderWidth, mViewFinderHeight;
+ boolean mPreviewing = false;
+
+ MediaPlayer mClickSound;
+
+ Capturer mCaptureObject;
+ ImageCapture mImageCapture = null;
+
+ boolean mPausing = false;
+
+ boolean mIsFocusing = false;
+ boolean mIsFocused = false;
+ boolean mIsFocusButtonPressed = false;
+ boolean mCaptureOnFocus = false;
+
+ static ContentResolver mContentResolver;
+ boolean mDidRegister = false;
+
+ int mCurrentZoomIndex = 0;
+
+ private static final int STILL_MODE = 1;
+ private static final int VIDEO_MODE = 2;
+ private int mMode = STILL_MODE;
+
+ ArrayList<MenuItem> mGalleryItems = new ArrayList<MenuItem>();
+
+ boolean mMenuSelectionMade;
+
+ View mPostPictureAlert;
+ LocationManager mLocationManager = null;
+
+ private Animation mFocusBlinkAnimation;
+ private View mFocusIndicator;
+ private ToneGenerator mFocusToneGenerator;
+
+ private ShutterCallback mShutterCallback = new ShutterCallback();
+ private RawPictureCallback mRawPictureCallback = new RawPictureCallback();
+ private JpegPictureCallback mJpegPictureCallback = new JpegPictureCallback();
+ private AutoFocusCallback mAutoFocusCallback = new AutoFocusCallback();
+ private long mShutterPressTime;
+ private int mPicturesRemaining;
+
+ private boolean mKeepAndRestartPreview;
+
+ private Handler mHandler = new MainHandler();
+ private ProgressDialog mSavingProgress;
+
+ 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);
+ }
+ 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
+ // TODO put up a "please wait" message
+ // TODO also listen for the media scanner finished message
+ showStorageToast();
+ } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+ // SD card unavailable
+ showStorageToast();
+ } 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)) {
+ showStorageToast();
+ }
+ }
+ };
+
+ 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 long mRawPictureCallbackTime;
+
+ private boolean mImageSavingItem = false;
+
+ private final class ShutterCallback implements android.hardware.Camera.ShutterCallback {
+ public void onShutter() {
+ if (DEBUG) {
+ long now = System.currentTimeMillis();
+ Log.v(TAG, "********** Total shutter lag " + (now - mShutterPressTime) + " ms");
+ }
+ if (mClickSound != null) {
+ mClickSound.seekTo(0);
+ mClickSound.start();
+ }
+ }
+ };
+
+ 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();
+ mBlackout.setVisibility(View.INVISIBLE);
+ if (!isPickIntent() && mPreferences.getBoolean("pref_camera_postpicturemenu_key", true)) {
+ mPostPictureAlert.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+
+ private final class JpegPictureCallback implements PictureCallback {
+ public void onPictureTaken(byte [] jpegData, android.hardware.Camera camera) {
+ if (Config.LOGV)
+ Log.v(TAG, "got JpegPictureCallback...");
+
+ mImageCapture.storeImage(jpegData, camera);
+
+ mStatus = SNAPSHOT_COMPLETED;
+
+ if (!mPreferences.getBoolean("pref_camera_postpicturemenu_key", true)) {
+ if (mKeepAndRestartPreview) {
+ long delay = 1500 - (System.currentTimeMillis() - mRawPictureCallbackTime);
+ mHandler.sendEmptyMessageDelayed(RESTART_PREVIEW, Math.max(delay, 0));
+ }
+ return;
+ }
+
+ if (mKeepAndRestartPreview) {
+ mKeepAndRestartPreview = false;
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+
+ // Post this message so that we can finish processing the request. This also
+ // prevents the preview from showing up before mPostPictureAlert is dismissed.
+ mHandler.sendEmptyMessage(RESTART_PREVIEW);
+ }
+
+ }
+ };
+
+ private final class AutoFocusCallback implements android.hardware.Camera.AutoFocusCallback {
+ public void onAutoFocus(boolean focused, android.hardware.Camera camera) {
+ 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 {
+ mFocusToneGenerator.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) {
+ try {
+ if (DEBUG) {
+ startTiming();
+ }
+
+ mLastContentUri = ImageManager.instance().addImage(
+ Camera.this,
+ mContentResolver,
+ DateFormat.format("yyyy-MM-dd kk.mm.ss", System.currentTimeMillis()).toString(),
+ "",
+ System.currentTimeMillis(),
+ // location for the database goes here
+ null,
+ 0, // the dsp will use the right orientation so don't "double set it"
+ ImageManager.CAMERA_IMAGE_BUCKET_NAME,
+ null);
+
+ 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) {
+ 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) {
+ boolean captureOnly = isPickIntent();
+
+ if (!captureOnly) {
+ storeImage(data);
+ sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", mLastContentUri));
+ } else {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 4;
+
+ if (DEBUG) {
+ startTiming();
+ }
+
+ mCaptureOnlyBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
+
+ if (DEBUG) {
+ stopTiming();
+ Log.d(TAG, "Decoded mCaptureOnly bitmap (" + mCaptureOnlyBitmap.getWidth() +
+ "x" + mCaptureOnlyBitmap.getHeight() + " ) in " +
+ (mWallTimeEnd - mWallTimeStart) + " ms. Thread time was " +
+ ((mThreadTimeEnd - mThreadTimeStart) / 1000000) + " ms.");
+ }
+
+ openOptionsMenu();
+ }
+
+
+ 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;
+ android.hardware.Camera.Parameters parameters = mCameraDevice.getParameters();
+ // 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.
+ parameters.set("jpeg-quality", 85);
+ parameters.set("rotation", latchedOrientation);
+ if (loc != null) {
+ parameters.set("gps-latitude", String.valueOf(loc.getLatitude()));
+ parameters.set("gps-longitude", String.valueOf(loc.getLongitude()));
+ parameters.set("gps-altitude", String.valueOf(loc.getAltitude()));
+ parameters.set("gps-timestamp", String.valueOf(loc.getTime()));
+ } else {
+ parameters.remove("gps-latitude");
+ parameters.remove("gps-longitude");
+ parameters.remove("gps-altitude");
+ parameters.remove("gps-timestamp");
+ }
+
+ Size pictureSize = parameters.getPictureSize();
+ Size previewSize = parameters.getPreviewSize();
+
+ // resize the SurfaceView to the aspect-ratio of the still image
+ // and so that we can see the full image that was taken
+ ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
+ if (pictureSize.width*previewSize.height < previewSize.width*pictureSize.height) {
+ lp.width = (pictureSize.width * previewSize.height) / pictureSize.height;
+ } else {
+ lp.height = (pictureSize.height * previewSize.width) / pictureSize.width;
+ }
+ mSurfaceView.requestLayout();
+
+ mCameraDevice.setParameters(parameters);
+
+ mCameraDevice.takePicture(mShutterCallback, mRawPictureCallback, mJpegPictureCallback);
+
+ mBlackout.setVisibility(View.VISIBLE);
+ // Comment this out for now until we can decode the preview frame. This currently
+ // just animates black-on-black because the surface flinger blacks out the surface
+ // when the camera driver detaches the buffers.
+ if (false) {
+ Animation a = new android.view.animation.TranslateAnimation(mBlackout.getWidth(), 0 , 0, 0);
+ a.setDuration(450);
+ a.startNow();
+ mBlackout.setAnimation(a);
+ }
+ }
+
+ public void onSnap() {
+ // 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);
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ 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 (DEBUG) mShutterPressTime = System.currentTimeMillis();
+ if (mPicturesRemaining < 1) {
+ showStorageToast();
+ return;
+ }
+
+ mStatus = SNAPSHOT_IN_PROGRESS;
+
+ mKeepAndRestartPreview = !mPreferences.getBoolean("pref_camera_postpicturemenu_key", true);
+
+ boolean getContentAction = isPickIntent();
+ if (getContentAction) {
+ mImageCapture.initiate(true);
+ } else {
+ mImageCapture.initiate(false);
+ }
+ }
+ }
+
+ 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);
+
+ mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ mContentResolver = getContentResolver();
+
+ //setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
+ requestWindowFeature(Window.FEATURE_PROGRESS);
+
+ Window win = getWindow();
+ win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.camera);
+
+ mSurfaceView = (SurfaceView) 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 = (ImageView) findViewById(R.id.blackout);
+ mBlackout.setBackgroundDrawable(new ColorDrawable(0xFF000000));
+
+ mPostPictureAlert = findViewById(R.id.post_picture_panel);
+ View b;
+
+ b = findViewById(R.id.save);
+ b.setOnClickListener(this);
+
+ b = findViewById(R.id.discard);
+ b.setOnClickListener(this);
+
+ b = findViewById(R.id.share);
+ b.setOnClickListener(this);
+
+ b = findViewById(R.id.setas);
+ b.setOnClickListener(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_SYSTEM);
+ mClickSound.prepare();
+ }
+ } catch (Exception ex) {
+ Log.w(TAG, "Couldn't create click sound", ex);
+ }
+
+ mOrientationListener = new OrientationListener(this) {
+ public void onOrientationChanged(int orientation) {
+ mLastOrientation = orientation;
+ }
+ };
+
+ mFocusIndicator = findViewById(R.id.focus_indicator);
+ mFocusBlinkAnimation = AnimationUtils.loadAnimation(this, R.anim.auto_focus_blink);
+ mFocusBlinkAnimation.setRepeatCount(Animation.INFINITE);
+ mFocusBlinkAnimation.setRepeatMode(Animation.REVERSE);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ final View hintView = findViewById(R.id.hint_toast);
+ if (hintView != null)
+ hintView.setVisibility(View.GONE);
+
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+ final boolean storageOK = calculatePicturesRemaining() > 0;
+ if (hintView == null)
+ return;
+
+ if (storageOK) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ hintView.setVisibility(View.VISIBLE);
+ }
+ });
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ Animation a = new android.view.animation.AlphaAnimation(1F, 0F);
+ a.setDuration(500);
+ a.startNow();
+ hintView.setAnimation(a);
+ hintView.setVisibility(View.GONE);
+ }
+ }, 3000);
+ } else {
+ mHandler.post(new Runnable() {
+ public void run() {
+ hintView.setVisibility(View.GONE);
+ showStorageToast();
+ }
+ });
+ }
+ }
+ });
+ t.start();
+ }
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.save: {
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ postAfterKeep(null);
+ break;
+ }
+
+ case R.id.discard: {
+ if (mCaptureObject != null) {
+ mCaptureObject.cancelSave();
+ Uri uri = mCaptureObject.getLastCaptureUri();
+ if (uri != null) {
+ mContentResolver.delete(uri, null, null);
+ }
+ mCaptureObject.dismissFreezeFrame(true);
+ }
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ break;
+ }
+
+ case R.id.share: {
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ postAfterKeep(new Runnable() {
+ public void run() {
+ Uri u = mCaptureObject.getLastCaptureUri();
+ 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(Camera.this, R.string.no_way_to_share_video, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ break;
+ }
+
+ case R.id.setas: {
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ postAfterKeep(new Runnable() {
+ public void run() {
+ Uri u = mCaptureObject.getLastCaptureUri();
+ 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(Camera.this, R.string.no_way_to_share_video, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ break;
+ }
+ }
+ }
+
+ void keepVideo() {
+ };
+
+ private void showStorageToast() {
+ String noStorageText = null;
+ int remaining = calculatePicturesRemaining();
+
+ if (remaining == NO_STORAGE_ERROR) {
+ noStorageText = getString(R.string.no_storage);
+ } else if (remaining < 1) {
+ noStorageText = getString(R.string.not_enough_space);
+ }
+
+ if (noStorageText != null) {
+ Toast.makeText(this, noStorageText, 5000).show();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+
+ mPausing = false;
+ mOrientationListener.enable();
+
+ if (isPickIntent()) {
+ mMode = STILL_MODE;
+ }
+
+ // 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.addDataScheme("file");
+ registerReceiver(mReceiver, intentFilter);
+ mDidRegister = true;
+
+ mImageCapture = new ImageCapture();
+
+ restartPreview();
+
+ if (mPreferences.getBoolean("pref_camera_recordlocation_key", false))
+ startReceivingLocationUpdates();
+
+ updateFocusIndicator();
+
+ mFocusToneGenerator = new ToneGenerator(AudioManager.STREAM_SYSTEM, FOCUS_BEEP_VOLUME);
+
+ mBlackout.setVisibility(View.INVISIBLE);
+ }
+
+ private ImageManager.DataLocation dataLocation() {
+ return ImageManager.DataLocation.EXTERNAL;
+ }
+
+ @Override
+ public void onStop() {
+ keep();
+ stopPreview();
+ closeCamera();
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ super.onStop();
+ }
+
+ @Override
+ protected void onPause() {
+ keep();
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+
+ mPausing = true;
+ mOrientationListener.disable();
+
+ stopPreview();
+
+ if (!mImageCapture.mCapturing) {
+ closeCamera();
+ }
+ if (mDidRegister) {
+ unregisterReceiver(mReceiver);
+ mDidRegister = false;
+ }
+ stopReceivingLocationUpdates();
+ mFocusToneGenerator.release();
+ mFocusToneGenerator = 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();
+ break;
+ }
+ }
+ }
+
+ private void autoFocus() {
+ updateFocusIndicator();
+ if (!mIsFocusing) {
+ if (mCameraDevice != null) {
+ mIsFocusing = true;
+ mIsFocused = false;
+ mCameraDevice.autoFocus(mAutoFocusCallback);
+ }
+ }
+ }
+
+ 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 || mStatus == SNAPSHOT_COMPLETED) {
+ if (mPostPictureAlert.getVisibility() == View.VISIBLE) {
+ keep();
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ restartPreview();
+ }
+ // ignore backs while we're taking a picture
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_FOCUS:
+ mIsFocusButtonPressed = true;
+ if (event.getRepeatCount() == 0) {
+ if (mPreviewing) {
+ autoFocus();
+ } else if (mCaptureObject != null) {
+ // Save and restart preview
+ mCaptureObject.onSnap();
+ }
+ }
+ return true;
+ case KeyEvent.KEYCODE_CAMERA:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (event.getRepeatCount() == 0) {
+ // 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 (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && !mIsFocusButtonPressed) {
+ // But we do need to start AF for DPAD_CENTER
+ autoFocus();
+ }
+ }
+ }
+ return true;
+ case KeyEvent.KEYCODE_MENU:
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ break;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_FOCUS:
+ clearFocus();
+ updateFocusIndicator();
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ if (mMode == STILL_MODE) {
+ // 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 void restartPreview() {
+ SurfaceView surfaceView = mSurfaceView;
+ if (surfaceView == null ||
+ surfaceView.getWidth() == 0 || surfaceView.getHeight() == 0) {
+ return;
+ }
+ // make sure the surfaceview fills the whole screen when previewing
+ ViewGroup.LayoutParams lp = surfaceView.getLayoutParams();
+ lp.width = ViewGroup.LayoutParams.FILL_PARENT;
+ lp.height = ViewGroup.LayoutParams.FILL_PARENT;
+ surfaceView.requestLayout();
+ setViewFinder(mViewFinderWidth, mViewFinderHeight, 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();
+ }
+
+ 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 (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
+ mCameraDevice.setPreviewDisplay(mSurfaceHolder);
+
+
+ // request the preview size, the hardware may not honor it,
+ // if we depended on it we would have to query the size again
+ android.hardware.Camera.Parameters p = mCameraDevice.getParameters();
+ p.setPreviewSize(w, h);
+ mCameraDevice.setParameters(p);
+
+
+ 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");
+ mCameraDevice.startPreview();
+ 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() {
+ Uri target = mMode == STILL_MODE ? Images.Media.INTERNAL_CONTENT_URI
+ : Video.Media.INTERNAL_CONTENT_URI;
+ Intent intent = new Intent(Intent.ACTION_VIEW, target);
+ startActivity(intent);
+ }
+
+ 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 worst to best order
+ for (int i = 0; i < mLocationListeners.length && l == null; i++) {
+ l = mLocationListeners[i].current();
+ }
+
+ 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) {
+ mKeepAndRestartPreview = false;
+ mHandler.removeMessages(RESTART_PREVIEW);
+ 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 (mMode == STILL_MODE) {
+ 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;
+ }
+ } else if (mMode == VIDEO_MODE) {
+ }
+
+ if (mCaptureObject != null)
+ mCaptureObject.cancelAutoDismiss();
+
+ return true;
+ }
+
+ private boolean isPickIntent() {
+ String action = getIntent().getAction();
+ return (Intent.ACTION_PICK.equals(action) || MediaStore.ACTION_IMAGE_CAPTURE.equals(action));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ if (isPickIntent()) {
+ menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_SELECT_PHOTOS , 0, R.string.camera_selectphoto).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Bitmap bitmap = mImageCapture.getLastBitmap();
+ mCaptureObject.setDone(true);
+
+ // TODO scale the image down to something ridiculous until IPC gets straightened out
+ float scale = .5F;
+ Matrix m = new Matrix();
+ m.setScale(scale, scale);
+
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0,
+ bitmap.getWidth(),
+ bitmap.getHeight(),
+ m, true);
+
+ Bundle myExtras = getIntent().getExtras();
+ String cropValue = myExtras != null ? myExtras.getString("crop") : null;
+ if (cropValue != null) {
+ Bundle newExtras = new Bundle();
+ if (cropValue.equals("circle"))
+ newExtras.putString("circleCrop", "true");
+ newExtras.putParcelable("data", bitmap);
+
+ Intent cropIntent = new Intent();
+ cropIntent.setClass(Camera.this, CropImage.class);
+ cropIntent.putExtras(newExtras);
+ startActivityForResult(cropIntent, CROP_MSG);
+ } else {
+ Bundle extras = new Bundle();
+ extras.putParcelable("data", bitmap);
+ setResult(RESULT_OK, new Intent("inline-data")
+ .putExtra("data", bitmap));
+ finish();
+ }
+ return true;
+ }
+ });
+
+ menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_NEW_PHOTO, 0, R.string.camera_takenewphoto).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ keep();
+ return true;
+ }
+ });
+ menu.add(MenuHelper.VIDEO_SAVING_ITEM, MENU_SAVE_TAKE_NEW_VIDEO, 0, R.string.camera_takenewvideo).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ toss();
+ return true;
+ }
+ });
+ } else {
+ addBaseMenuItems(menu);
+ MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_ROTATE_MENU,
+ 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);
+
+ mGalleryItems.add(menu.add(MenuHelper.VIDEO_SAVING_ITEM, MENU_SAVE_GALLERY_VIDEO_PHOTO, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ keepVideo();
+ gotoGallery();
+ return true;
+ }
+ }));
+ }
+ return true;
+ }
+
+ SelectedImageGetter mSelectedImageGetter =
+ new SelectedImageGetter() {
+ public ImageManager.IImage getCurrentImage() {
+ return getImageForURI(getCurrentImageUri());
+ }
+ public Uri getCurrentImageUri() {
+ keep();
+ return mCaptureObject.getLastCaptureUri();
+ }
+ };
+
+ private int calculatePicturesRemaining() {
+ try {
+ if (!ImageManager.instance().hasStorage()) {
+ mPicturesRemaining = NO_STORAGE_ERROR;
+ } else {
+ String storageDirectory = Environment.getExternalStorageDirectory().toString();
+ StatFs stat = new StatFs(storageDirectory);
+ float remaining = ((float)stat.getAvailableBlocks() * (float)stat.getBlockSize()) / 400000F;
+ mPicturesRemaining = (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.
+ mPicturesRemaining = CANNOT_STAT_ERROR;
+ }
+ return mPicturesRemaining;
+ }
+
+ private void addBaseMenuItems(Menu menu) {
+ 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 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..ccf5821
--- /dev/null
+++ b/src/com/android/camera/CameraButtonIntentReceiver.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+
+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..fb77e34
--- /dev/null
+++ b/src/com/android/camera/CameraSettings.java
@@ -0,0 +1,65 @@
+/*
+ * 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.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.content.Context;
+
+/**
+ * CameraSettings
+ */
+class CameraSettings extends PreferenceActivity
+{
+ public CameraSettings()
+ {
+ }
+
+ protected int resourceId() {
+ return R.xml.camera_preferences;
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ addPreferencesFromResource(resourceId());
+
+ Preference p = findPreference("pref_camera_upload_albumname_key");
+ if (p != null) {
+ SharedPreferences sp = p.getSharedPreferences();
+ p.setSummary(
+ String.format(getResources().getString(R.string.pref_camera_upload_albumname_summary),
+ p.getSharedPreferences().getString(p.getKey(), UploadService.sUploadAlbumName)));
+ p.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference p, Object newObjValue) {
+ String newValue = (String) newObjValue;
+ if (newValue == null || newValue.length() == 0)
+ return false;
+ if (android.util.Config.LOGV)
+ android.util.Log.v("camera", "onPreferenceChange ... " + newValue);
+ p.setSummary(String.format(getResources().getString(R.string.pref_camera_upload_albumname_summary), newValue));
+ return true;
+ }
+ });
+ }
+ }
+}
+
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..a7f6404
--- /dev/null
+++ b/src/com/android/camera/CropImage.java
@@ -0,0 +1,794 @@
+/*
+ * 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.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);
+
+ 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("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);
+
+ // 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() {
+ 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) {
+ 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..de2fd5c
--- /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..9f1664b
--- /dev/null
+++ b/src/com/android/camera/GalleryPicker.java
@@ -0,0 +1,607 @@
+/*
+ * 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.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;
+
+public class GalleryPicker extends Activity {
+ static private final String TAG = "GalleryPicker";
+
+ GridView mGridView;
+ Drawable mFrameGalleryMask;
+ Drawable mCellOutline;
+
+ BroadcastReceiver mReceiver;
+ GalleryPickerAdapter mAdapter;
+
+ Dialog mMediaScanningDialog;
+
+ MenuItem mFlipItem;
+ 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);
+ }
+ mAdapter.notifyDataSetChanged();
+ mAdapter.init(!unmounted && !scanning);
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ setContentView(R.layout.gallerypicker);
+
+ 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) {
+ menu.setHeaderTitle(
+ mAdapter.baseTitleForPosition(((AdapterContextMenuInfo)menuInfo).position));
+ 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.mFirstImageUris) {
+ if (position >= mAdapter.mFirstImageUris.size()) {
+ // the list of ids does not include the "all" list
+ targetUri = mAdapter.firstImageUri(mAdapter.mIds.get(position-1));
+ } else {
+ // the mFirstImageUris list includes the "all" uri
+ targetUri = mAdapter.mFirstImageUris.get(position);
+ }
+ }
+ if (targetUri != null && position > 0) {
+ targetUri = targetUri.buildUpon().appendQueryParameter("bucketId", mAdapter.mIds.get(info.position-1)).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;
+ }
+ });
+ }
+ });
+ }
+
+ private void launchFolderGallery(int position) {
+ android.net.Uri uri = Images.Media.INTERNAL_CONTENT_URI;
+ if (position > 0) {
+ uri = uri.buildUpon().appendQueryParameter("bucketId", mAdapter.mIds.get(position-1)).build();
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ if (position > 0) {
+ intent.putExtra("windowTitle", mAdapter.mNames.get(position-1));
+ }
+ startActivity(intent);
+ }
+
+ class ItemInfo {
+ Bitmap bitmap;
+ int count;
+ int overlayId;
+ }
+
+ class GalleryPickerAdapter extends BaseAdapter {
+ ArrayList<String> mIds = new ArrayList<String>();
+ ArrayList<String> mNames = new ArrayList<String>();
+ ArrayList<Uri> mFirstImageUris = new ArrayList<Uri>();
+
+ ArrayList<View> mAllViews = new ArrayList<View>();
+ SparseArray<ItemInfo> mThumbs = new SparseArray<ItemInfo>();
+
+ boolean mDone = false;
+ CameraThread mWorkerThread;
+
+ public void init(boolean assumeMounted) {
+ mAllViews.clear();
+ mThumbs.clear();
+
+ ImageManager.IImageList images;
+ if (assumeMounted) {
+ images = ImageManager.instance().allImages(
+ GalleryPicker.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_DESCENDING);
+ } else {
+ images = ImageManager.instance().emptyImageList();
+ }
+
+ mIds.clear();
+ mNames.clear();
+ mFirstImageUris.clear();
+
+ 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 (String key : hashMap.keySet()) {
+ if (key.equals(cameraItem)) {
+ cameraBucketId = key;
+ } else {
+ mIds.add(key);
+ }
+ }
+ images.deactivate();
+ notifyDataSetInvalidated();
+
+ // sort baesd on the display name. if two display names compare equal
+ // then sort based on the id
+ java.util.Collections.sort(mIds, new java.util.Comparator<String>() {
+ public int compare(String first, String second) {
+ int x = hashMap.get(first).compareTo(hashMap.get(second));
+ if (x == 0)
+ x = first.compareTo(second);
+ return x;
+ }
+ });
+
+ for (String s : mIds) {
+ mNames.add(hashMap.get(s));
+ }
+
+ if (cameraBucketId != null) {
+ mIds.add(0, cameraBucketId);
+ mNames.add(0, "Camera");
+ }
+ final boolean foundCameraBucket = cameraBucketId != null;
+
+ mDone = false;
+ mWorkerThread = new CameraThread(new Runnable() {
+ public void run() {
+ try {
+ // no images, nothing to do
+ if (mIds.size() == 0)
+ return;
+
+ for (int i = 0; i < mIds.size() + 1 && !mDone; i++) {
+ String id = i == 0 ? null : mIds.get(i-1);
+ ImageManager.IImageList list = ImageManager.instance().allImages(
+ GalleryPicker.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_DESCENDING,
+ id);
+ try {
+ if (mPausing) {
+ break;
+ }
+ if (list.getCount() > 0)
+ mFirstImageUris.add(i, list.getImageAt(0).fullSizeImageUri());
+
+ int overlay = -1;
+ if (i == 1 && foundCameraBucket)
+ overlay = R.drawable.frame_overlay_gallery_camera;
+ final Bitmap b = makeMiniThumbBitmap(142, 142, list);
+ final int pos = i;
+ final int count = list.getCount();
+ final int overlayId = overlay;
+ 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;
+ info.overlayId = overlayId;
+ mThumbs.put(pos, 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.toString());
+ }
+ }
+ });
+ mWorkerThread.start();
+ mWorkerThread.toBackground();
+ }
+
+ Uri firstImageUri(String id) {
+ ImageManager.IImageList list = ImageManager.instance().allImages(
+ GalleryPicker.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_DESCENDING,
+ id);
+ Uri uri = list.getImageAt(0).fullSizeImageUri();
+ list.deactivate();
+ return uri;
+ }
+
+ public int getCount() {
+ return mIds.size() + 1; // add 1 for the everything bucket
+ }
+
+ public Object getItem(int position) {
+ return null;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ private String baseTitleForPosition(int position) {
+ if (position == 0) {
+ return getResources().getString(R.string.all_images);
+ } else {
+ return mNames.get(position-1);
+ }
+ }
+
+ 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);
+ ItemInfo info = mThumbs.get(position);
+ if (info != null) {
+ iv.setImageBitmap(info.bitmap);
+ iv.setOverlay(info.overlayId);
+ String title = baseTitleForPosition(position) + " (" + info.count + ")";
+ titleView.setText(title);
+ } else {
+ iv.setImageResource(android.R.color.transparent);
+ iv.setOverlay(-1);
+ 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);
+
+ // 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 (!scanning && mAdapter.mIds.size() <= 1) {
+ android.net.Uri uri = Images.Media.INTERNAL_CONTENT_URI;
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(intent);
+ finish();
+ return;
+ }
+ }
+
+
+ private void setBackgrounds(Resources r) {
+ mFrameGalleryMask = r.getDrawable(R.drawable.frame_gallery_preview_album_mask);
+
+ mCellOutline = r.getDrawable(android.R.drawable.gallery_thumb);
+ }
+
+ 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;
+ if (count < 4) {
+ count = 1;
+ // uncomment for 2 pictures per frame
+// if (count == 2 || count == 3) {
+// count = 2;
+// imageWidth = imageWidth * 2 / 3;
+// imageHeight = imageHeight * 2 / 3;
+// offsetWidth = imageWidth / 3 - padding;
+// offsetHeight = -imageHeight / 3 + padding * 2;
+ } else if (count >= 4) {
+ count = 4;
+ 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 < count; i++) {
+ if (mPausing) {
+ return null;
+ }
+ ImageManager.IImage image = i < count ? images.getImageAt(i) : null;
+ if (image == null) {
+ break;
+ }
+ Bitmap temp = image.miniThumbBitmap();
+ if (temp != null) {
+ 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);
+
+ mFlipItem = MenuHelper.addFlipOrientation(menu, this, mPrefs);
+
+ menu.add(0, 0, 0, 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;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(android.view.Menu menu) {
+ int keyboard = getResources().getConfiguration().keyboardHidden;
+ mFlipItem.setEnabled(keyboard == android.content.res.Configuration.KEYBOARDHIDDEN_YES);
+
+ return true;
+ }
+}
diff --git a/src/com/android/camera/GalleryPickerItem.java b/src/com/android/camera/GalleryPickerItem.java
new file mode 100644
index 0000000..3fc9678
--- /dev/null
+++ b/src/com/android/camera/GalleryPickerItem.java
@@ -0,0 +1,98 @@
+/*
+ * 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);
+ }
+
+ @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..3af6867
--- /dev/null
+++ b/src/com/android/camera/GallerySettings.java
@@ -0,0 +1,46 @@
+/*
+ * 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.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.content.Context;
+
+/**
+ * GallerySettings
+ */
+class GallerySettings extends CameraSettings
+{
+ public GallerySettings()
+ {
+ }
+
+ @Override
+ protected int resourceId() {
+ return R.xml.gallery_preferences;
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ }
+}
+
diff --git a/src/com/android/camera/HighlightView.java b/src/com/android/camera/HighlightView.java
new file mode 100644
index 0000000..594bab6
--- /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..c8abdae
--- /dev/null
+++ b/src/com/android/camera/ImageGallery2.java
@@ -0,0 +1,1734 @@
+/*
+ * 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.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+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.widget.Scroller;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+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 mFlipItem;
+ private SharedPreferences mPrefs;
+
+ 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()) {
+ mGvs.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ if (mSelectedImageGetter.getCurrentImage() == null)
+ return;
+
+ 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(R.string.context_menu_header);
+ if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
+ MenuHelper.MenuItemsResult r = MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL,
+ ImageGallery2.this,
+ mHandler,
+ mDeletePhotoRunnable,
+ new MenuHelper.MenuInvoker() {
+ public void run(MenuHelper.MenuCallback cb) {
+ cb.run(mSelectedImageGetter.getCurrentImageUri(), mSelectedImageGetter.getCurrentImage());
+
+ mGvs.clearCache();
+ mGvs.invalidate();
+ mGvs.start();
+ mNoImagesView.setVisibility(mAllImages.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+ });
+ if (r != null)
+ r.gettingReadyToOpen(menu, mSelectedImageGetter.getCurrentImage());
+
+ addSlideShowMenu(menu, 1000);
+ }
+
+ if ((mInclusion & ImageManager.INCLUDE_VIDEOS) != 0) {
+ MenuHelper.MenuItemsResult r = MenuHelper.addVideoMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL,
+ ImageGallery2.this,
+ mHandler,
+ mSelectedImageGetter,
+ new Runnable() {
+ public void run() {
+ ImageManager.IImage image = mSelectedImageGetter.getCurrentImage();
+ if (image != null) {
+ mGvs.clearCache();
+ mAllImages.removeImage(mSelectedImageGetter.getCurrentImage());
+ mGvs.invalidate();
+ mGvs.start();
+ mNoImagesView.setVisibility(mAllImages.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+ }
+ },
+ null, null);
+ if (r != null)
+ r.gettingReadyToOpen(menu, mSelectedImageGetter.getCurrentImage());
+ }
+ }
+ });
+ }
+ }
+
+ 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();
+ mAllImages.removeImage(mSelectedImageGetter.getCurrentImage());
+ mGvs.invalidate();
+ mGvs.start();
+ mNoImagesView.setVisibility(mAllImages.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+ };
+
+ 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.showContextMenu();
+ }
+ };
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ // 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();
+ 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:
+ mHandler.postDelayed(mLongPressCallback, ViewConfiguration.getLongPressTimeout());
+ break;
+ case KeyEvent.KEYCODE_DEL:
+ MenuHelper.deletePhoto(this, mDeletePhotoRunnable);
+ 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);
+ 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();
+
+ 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 CROP_MSG: {
+ if (Config.LOGV) Log.v(TAG, "onActivityResult " + data);
+ 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();
+ mAllImages.deactivate();
+ mGvs.onPause();
+
+ if (mReceiver != null) {
+ unregisterReceiver(mReceiver);
+ mReceiver = null;
+ }
+ }
+
+ 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();
+ boolean loadingVideos = mInclusion == ImageManager.INCLUDE_VIDEOS;
+ 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;
+ }
+ };
+ allImages(true).checkThumbnails(r);
+ 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 (false) {
+ if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
+ item = menu.add(0, 0, 0, R.string.upload_all);
+ item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ UploadAction.uploadImage(ImageGallery2.this, null);
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_upload);
+ }
+ }
+ addSlideShowMenu(menu, 0);
+
+ mFlipItem = MenuHelper.addFlipOrientation(menu, this, mPrefs);
+
+ 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) {
+ int keyboard = getResources().getConfiguration().keyboardHidden;
+ mFlipItem.setEnabled(keyboard == android.content.res.Configuration.KEYBOARDHIDDEN_YES);
+
+ return true;
+ }
+
+ private synchronized ImageManager.IImageList allImages(boolean assumeMounted) {
+ if (mAllImages == null) {
+ mNoImagesView = findViewById(R.id.no_images);
+
+ mInclusion = ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS;
+ ImageManager.DataLocation location = ImageManager.DataLocation.ALL;
+
+ 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/*")) {
+ }
+ }
+ Bundle extras = intent.getExtras();
+ String title = extras!= null ? extras.getString("windowTitle") : null;
+ if (title != null && title.length() > 0) {
+ leftText.setText(title);
+ }
+
+ if (extras != null && extras.getBoolean("pick-drm")) {
+ Log.d(TAG, "pick-drm is true");
+ mInclusion = ImageManager.INCLUDE_DRM_IMAGES;
+ location = ImageManager.DataLocation.INTERNAL;
+ }
+ }
+ 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 mDirectionBiasDown = true;
+ private final static boolean sDump = false;
+
+ 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(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);
+ } else {
+ select(-1);
+ }
+ 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);
+ 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);
+ scrollBy(0, (int)distanceY);
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ super.onShowPress(e);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ 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();
+ }
+
+ public void select(int newSel) {
+ int oldSel = mCurrentSelection;
+ if (oldSel == newSel)
+ return;
+
+ mShowSelection = (newSel != -1);
+ mCurrentSelection = newSel;
+ 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 mErrorBitmap;
+
+ 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) {
+ 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() {
+ if (mErrorBitmap == null) {
+ mErrorBitmap = BitmapFactory.decodeResource(GridViewSpecial.this.getResources(),
+ android.R.drawable.ic_menu_report_image);
+ }
+ return mErrorBitmap;
+ }
+
+ 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();
+ 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);
+ }
+ 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) {
+ if (pos == mCurrentSelection && mShowSelection) {
+ mCellOutline.setState(ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET);
+ } else {
+ mCellOutline.setState(EMPTY_STATE_SET);
+ }
+ 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);
+
+ // this should be unnecessary but if you remove this line then executing
+ // the subsequent startActivity causes the user to have to choose among
+ // ViewImage and a number of bogus entries (like attaching the image to
+ // a contact).
+ intent.setClass(mContext, ViewImage.class);
+ 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..f3e04d7
--- /dev/null
+++ b/src/com/android/camera/ImageLoader.java
@@ -0,0 +1,348 @@
+/*
+ * 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.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.util.Config;
+import android.util.Log;
+
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+class ImageLoader {
+ private static final String TAG = "ImageLoader";
+
+ // queue of work to do in the workder thread
+ private ArrayList<WorkItem> mQueue = new ArrayList<WorkItem>();
+ private ArrayList<WorkItem> mInProgress = new ArrayList<WorkItem>();
+
+ // array of image id's that have bad thumbnails
+ private ArrayList<Uri> mBadThumbnailList = new ArrayList<Uri>();
+
+ // 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) {
+ Log.e(TAG, "couldn't load miniThumbBitmap " + ex.toString());
+ // sd card removal??
+ }
+ if (b == null) {
+ if (Config.LOGV) Log.v(TAG, "unable to read thumbnail for " + workItem.mImage.fullSizeImageUri());
+ mBadThumbnailList.add(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..8d3f90a
--- /dev/null
+++ b/src/com/android/camera/ImageManager.java
@@ -0,0 +1,3632 @@
+/*
+ * 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.Matrix;
+import android.location.Location;
+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.Images;
+import android.provider.MediaStore.MediaColumns;
+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 = "/sdcard/dcim/camera";
+ public static final String CAMERA_IMAGE_BUCKET_ID = String.valueOf(CAMERA_IMAGE_BUCKET_NAME.hashCode());
+
+ // 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) {
+ dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow());
+ 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) {
+ 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) {
+ 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;
+
+ 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) {
+ 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) {
+ 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. Synchonize 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) {
+ 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) {
+ saveMiniThumbToFile(bitmap, id, magic);
+ bitmap.recycle();
+ }
+
+ 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) {
+ Cursor c = Images.Media.query(
+ mContentResolver,
+ mBaseUri,
+ new String[] { "_id", "mini_thumb_magic" },
+ "mini_thumb_magic isnull and " + sWhereClause,
+ sAcceptableImageTypes,
+ "_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 max = 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, max)) {
+ 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
+ }
+ }
+ }
+
+ 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;
+ mCursor.deactivate();
+ 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;
+ }
+ }
+ 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;
+ }
+ 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 false;
+ }
+
+
+ /* (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 source, long id, long magic) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return;
+
+ long pos = id * sBytesPerMiniThumb;
+ long t0 = System.currentTimeMillis();
+ synchronized (r) {
+ try {
+ long t1 = System.currentTimeMillis();
+ byte [] data = miniThumbData(source);
+ 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());
+ }
+ }
+ }
+
+ 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);
+
+ public abstract void commitChanges();
+
+ public abstract void deactivate();
+
+ /**
+ * Returns the count of image objects.
+ *
+ * @return the number of images
+ */
+ public abstract int getCount();
+
+ /**
+ * 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);;
+
+ 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 VideoObject extends Image {
+ public VideoObject() {
+ super(0, 0, null, null, 0, 0);
+ }
+
+ public String getTags() {
+ return null;
+ }
+
+ public String setTags(String tags) {
+ return null;
+ }
+ }
+
+ 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/png"))
+ 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);
+ saveMiniThumb(rotate(thumbnail, orientation));
+ 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;
+ Cursor c = mContainer.getCursor();
+ synchronized (c) {
+ mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow());
+ }
+
+ 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 + "=? or " + Images.Media.MIME_TYPE + "=?" + ")";
+ final static private String[] sAcceptableImageTypes = new String[] { "image/jpeg", "image/png" };
+
+ 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;
+ mBucketId = bucketId;
+
+ 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; }
+
+ 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));
+ }
+ pfd.close();
+ } catch (IOException ex) {
+ if (VERBOSE) Log.v(TAG, "got io exception " + ex);
+ return null;
+ }
+ 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) {
+ // 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);
+ }
+ }
+
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index) {
+ 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) {
+ // TODO this isn't quite right because we need to get the
+ // total from each sub item and provide that in the callback
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ for (int i = 0; i < length; i++)
+ sublist[i].checkThumbnails(cb);
+ }
+
+ 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;
+ }
+
+ // 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) {
+ int pos = -1;
+ while (++pos < mSubList.length) {
+ IImageList sub = mSubList[pos];
+ if (sub.removeImage(image)) {
+ return true;
+ }
+ }
+ 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 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);
+ }
+ pfdInput.close();
+ } catch (IOException ex) {
+ if (VERBOSE) Log.v(TAG, "got io exception " + ex);
+ return null;
+ }
+ 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);
+ }
+ }
+
+ /*
+ * 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;
+ /**
+ * 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;
+ }
+
+ static public byte [] miniThumbData(Bitmap source) {
+ if (source == null)
+ return null;
+
+ float scale;
+ if (source.getWidth() < source.getHeight()) {
+ scale = MINI_THUMB_TARGET_SIZE / (float)source.getWidth();
+ } else {
+ scale = MINI_THUMB_TARGET_SIZE / (float)source.getHeight();
+ }
+ Matrix matrix = new Matrix();
+ matrix.setScale(scale, scale);
+ Bitmap miniThumbnail = ImageLoader.transform(matrix, source,
+ MINI_THUMB_TARGET_SIZE, MINI_THUMB_TARGET_SIZE, false);
+
+ if (miniThumbnail != source) {
+ source.recycle();
+ }
+ 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;
+ }
+
+ 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;
+ }
+
+ 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);
+ String path = parentFile.toString().toLowerCase();
+ String name = parentFile.getName().toLowerCase();
+
+ values.put(Images.ImageColumns.BUCKET_ID, path.hashCode());
+ values.put(Images.ImageColumns.BUCKET_DISPLAY_NAME, name);
+ 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(com.android.camera.ImageManager.IImageList.ThumbCheckCallback cb) {
+ }
+
+ public void commitChanges() {
+ }
+
+ public void deactivate() {
+ }
+
+ public HashMap<String, String> getBucketIds() {
+ return new HashMap<String,String>();
+ }
+
+ public int getCount() {
+ return 0;
+ }
+
+ 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(com.android.camera.ImageManager.IImageList.OnChange changeCallback) {
+ }
+
+ public void setOnChangeListener(com.android.camera.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 (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;
+ }
+}
diff --git a/src/com/android/camera/ImageViewTouchBase.java b/src/com/android/camera/ImageViewTouchBase.java
new file mode 100644
index 0000000..9993373
--- /dev/null
+++ b/src/com/android/camera/ImageViewTouchBase.java
@@ -0,0 +1,547 @@
+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.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());
+ }
+ }
+
+ 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.05F;
+
+ // 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..033fc9c
--- /dev/null
+++ b/src/com/android/camera/MenuHelper.java
@@ -0,0 +1,521 @@
+/*
+ * 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.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Handler;
+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.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+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_IMAGE_SHARE = 10;
+ static public final int MENU_IMAGE_SHARE_EMAIL = 11;
+ static public final int MENU_IMAGE_SHARE_MMS = 12;
+ static public final int MENU_IMAGE_SHARE_PICASA =13;
+ 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_SHARE_MMS = 25;
+ static public final int MENU_VIDEO_SHARE_YOUTUBE = 26;
+ static public final int MENU_VIDEO_TOSS = 27;
+ static public final int MENU_IMAGE_SHARE_PICASA_ALL =28;
+
+ 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);
+ }
+
+ static MenuItemsResult addImageMenuItems(
+ Menu menu,
+ int inclusions,
+ 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 ((inclusions & INCLUDE_SHARE_MENU) != 0) {
+ if (Config.LOGV)
+ Log.v(TAG, ">>>>> add share");
+ MenuItem item = 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);
+ intent.setType(image.getMimeType());
+ intent.putExtra(Intent.EXTRA_STREAM, u);
+ try {
+ activity.startActivity(Intent.createChooser(intent,
+ activity.getText(R.string.sendImage)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(activity, R.string.no_way_to_share_image, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_share);
+ requiresNoDrmAccessItems.add(item);
+ }
+
+ if ((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 ((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) {
+ deletePhoto(activity, onDelete);
+ return true;
+ }
+ })
+ .setAlphabeticShortcut('d')
+ .setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ if ((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.startActivity(cropIntent);
+ }
+ });
+ return true;
+ }
+ });
+ autoCrop.setIcon(android.R.drawable.ic_menu_crop);
+ requiresWriteAccessItems.add(autoCrop);
+ }
+
+ if ((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_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());
+
+ java.io.InputStream data = image.fullSizeImageData();
+ String lengthString = "";
+ try {
+ long length = data.available();
+ lengthString =
+ android.content.Formatter.formatFileSize(activity, length);
+ data.close();
+ } catch (java.io.IOException ex) {
+
+ } finally {
+ }
+ ((TextView)d.findViewById(R.id.details_attrname_1)).setText(R.string.details_file_size);
+ ((TextView)d.findViewById(R.id.details_attrvalu_1)).setText(lengthString);
+
+ String dimensionsString = String.valueOf(image.getWidth() + " X " + image.getHeight());
+ ((TextView)d.findViewById(R.id.details_attrname_2)).setText(R.string.details_image_resolution);
+ ((TextView)d.findViewById(R.id.details_attrvalu_2)).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_attrname_3)).setText(R.string.details_date_taken);
+ ((TextView)d.findViewById(R.id.details_attrvalu_3)).setText(dateString);
+ } else {
+ d.findViewById(R.id.details_daterow).setVisibility(View.GONE);
+ }
+
+
+ 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);
+ }
+
+ 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 MenuItemsResult addVideoMenuItems(
+ Menu menu,
+ int inclusions,
+ final Activity activity,
+ final Handler handler,
+ final SelectedImageGetter mGetter,
+ final Runnable onDelete,
+ final Runnable preWork,
+ final Runnable postWork) {
+
+ if ((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) {
+ if (preWork != null)
+ preWork.run();
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, mGetter.getCurrentImageUri());
+ activity.startActivity(intent);
+
+ // don't do the postWork since we're launching another activity
+ return true;
+ }
+ });
+ }
+
+ if ((inclusions & INCLUDE_SHARE_MENU) != 0) {
+ MenuItem item = menu.add(VIDEO_SAVING_ITEM, 0, 0, R.string.camera_share).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Uri u = mGetter.getCurrentImageUri();
+ if (u == null)
+ return true;
+
+ if (preWork != null)
+ preWork.run();
+
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType(mGetter.getCurrentImage().getMimeType());
+ intent.putExtra(Intent.EXTRA_STREAM, u);
+ try {
+ activity.startActivity(Intent.createChooser(intent,
+ activity.getText(R.string.sendVideo)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(activity, R.string.no_way_to_share_video, Toast.LENGTH_SHORT).show();
+
+ if (postWork != null)
+ postWork.run();
+ }
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_share);
+ }
+
+ if ((inclusions & INCLUDE_DELETE_MENU) != 0) {
+ MenuItem deleteMenu = menu.add(VIDEO_SAVING_ITEM, MENU_VIDEO_TOSS, 0, R.string.camera_toss).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ if (preWork != null)
+ preWork.run();
+
+ activity.getContentResolver().delete(mGetter.getCurrentImageUri(), null, null);
+
+ if (onDelete != null)
+ onDelete.run();
+
+ if (postWork != null)
+ postWork.run();
+
+ return true;
+ }
+ });
+ deleteMenu.setIcon(android.R.drawable.ic_menu_delete);
+ deleteMenu.setAlphabeticShortcut('d');
+ }
+
+ return null;
+ }
+
+ static void deletePhoto(Activity activity, final Runnable onDelete) {
+ boolean confirm = android.preference.PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("pref_gallery_confirm_delete_key", true);
+ if (!confirm) {
+ if (onDelete != null)
+ onDelete.run();
+ } else {
+ android.app.AlertDialog.Builder b = new android.app.AlertDialog.Builder(activity);
+ b.setIcon(R.drawable.delete_image);
+ b.setTitle(R.string.confirm_delete_title);
+ b.setMessage(R.string.confirm_delete_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 MenuItem addFlipOrientation(Menu menu, final Activity activity, final SharedPreferences prefs) {
+ // position 41 after rotate
+ return menu
+ .add(Menu.CATEGORY_SECONDARY, 304, 41, R.string.flip_orientation)
+ .setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ int current = activity.getRequestedOrientation();
+ int newOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ if (current == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+ newOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt("nuorientation", newOrientation);
+ editor.commit();
+ requestOrientation(activity, prefs);
+ return true;
+ }
+ })
+ .setIcon(android.R.drawable.ic_menu_always_landscape_portrait);
+ }
+
+ static void requestOrientation(Activity activity, SharedPreferences prefs) {
+ 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.
+ activity.setRequestedOrientation(
+ req == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ ? android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER
+ : req);
+ }
+
+ static public class YouTubeUploadInfoDialog extends Dialog {
+ private CheckBox mPrivate;
+ private ImageManager.VideoObject mVideo;
+ private EditText mTitle;
+ private EditText mTags;
+ private EditText mDescription;
+ private Spinner mCategory;
+ private Button mUpload;
+
+ public YouTubeUploadInfoDialog(final Activity activity,
+ final ArrayList<String> categoriesShort,
+ final ArrayList<String> categoriesLong,
+ ImageManager.VideoObject video,
+ final Runnable postRunnable) {
+ super(activity, android.R.style.Theme_Dialog);
+ mVideo = video;
+ setContentView(R.layout.youtube_upload_info);
+ setTitle(R.string.upload_dialog_title);
+
+ mPrivate = (CheckBox)findViewById(R.id.public_or_private);
+ if (!mPrivate.isChecked()) {
+ mPrivate.setChecked(true);
+ }
+
+ mTitle = (EditText)findViewById(R.id.video_title);
+ mTags = (EditText)findViewById(R.id.video_tags);
+ mDescription = (EditText)findViewById(R.id.video_description);
+ mCategory = (Spinner)findViewById(R.id.category);
+
+ if (Config.LOGV)
+ Log.v(TAG, "setting categories in adapter");
+ android.widget.ArrayAdapter<String> categories = new android.widget.ArrayAdapter<String>(activity, android.R.layout.simple_spinner_item, categoriesLong);
+ categories.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mCategory.setAdapter(categories);
+
+ if (mVideo != null) {
+ mTitle.setText(mVideo.getTitle());
+ mTags.setText(mVideo.getTags());
+ mDescription.setText(mVideo.getDescription());
+ }
+
+ mUpload = (Button)findViewById(R.id.do_upload);
+ mUpload.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v)
+ {
+ if (mVideo != null) {
+ mVideo.setName(mTitle.getText().toString());
+ mVideo.setDescription(mDescription.getText().toString());
+ mVideo.setTags(mTags.getText().toString());
+ }
+
+ YouTubeUploadInfoDialog.this.dismiss();
+ UploadAction.uploadImage(activity, mVideo);
+ if (postRunnable != null)
+ postRunnable.run();
+ }
+ });
+ }
+ }
+}
+
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/PwaUpload.java b/src/com/android/camera/PwaUpload.java
new file mode 100644
index 0000000..8df08df
--- /dev/null
+++ b/src/com/android/camera/PwaUpload.java
@@ -0,0 +1,56 @@
+/*
+ * 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.Bundle;
+import android.util.Log;
+import android.net.Uri;
+
+/**
+ *
+ */
+public class PwaUpload extends Activity
+{
+ private static final String TAG = "camera";
+
+ @Override public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ ImageManager.IImageList imageList = ImageManager.instance().allImages(
+ this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES|ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_ASCENDING);
+ Uri uri = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
+ if (android.util.Config.LOGV)
+ Log.v(TAG, "uri is " + uri);
+ ImageManager.IImage imageObj = imageList.getImageForUri(uri);
+
+ if (android.util.Config.LOGV)
+ Log.v(TAG, "imageObj is " + imageObj);
+ if (imageObj != null) {
+ UploadAction.uploadImage(this, imageObj);
+ }
+ finish();
+ }
+
+ @Override public void onResume() {
+ super.onResume();
+ }
+}
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/SlideShow.java b/src/com/android/camera/SlideShow.java
new file mode 100644
index 0000000..ee6c7be
--- /dev/null
+++ b/src/com/android/camera/SlideShow.java
@@ -0,0 +1,425 @@
+/*
+ * 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) {
+ // 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 void deactivate() {
+ // nothing to do here
+ }
+ }
+
+}
diff --git a/src/com/android/camera/UploadAction.java b/src/com/android/camera/UploadAction.java
new file mode 100644
index 0000000..41cc351
--- /dev/null
+++ b/src/com/android/camera/UploadAction.java
@@ -0,0 +1,34 @@
+/*
+ * 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.content.Intent;
+import android.os.Bundle;
+import android.util.Config;
+import android.util.Log;
+
+public class UploadAction {
+ static private final String TAG = "UploadAction";
+
+ static public void uploadImage(Activity activity, ImageManager.IImage image) {
+ Bundle args = new Bundle();
+ if (image != null)
+ args.putString("imageuri", image.fullSizeImageUri().toString());
+ activity.startService(new Intent(activity, UploadService.class).putExtras(args));
+ }
+}
diff --git a/src/com/android/camera/UploadService.java b/src/com/android/camera/UploadService.java
new file mode 100644
index 0000000..9c7d2b0
--- /dev/null
+++ b/src/com/android/camera/UploadService.java
@@ -0,0 +1,1181 @@
+/*
+ * 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 com.android.camera.ImageManager.IImage;
+import com.android.internal.http.multipart.Part;
+import com.android.internal.http.multipart.MultipartEntity;
+import com.android.internal.http.multipart.PartBase;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.preference.PreferenceManager;
+import android.sax.Element;
+import android.sax.ElementListener;
+import android.sax.EndTextElementListener;
+import android.sax.RootElement;
+import android.util.Config;
+import android.util.Log;
+import android.util.Xml;
+
+import com.google.android.googleapps.GoogleLoginCredentialsResult;
+import com.google.android.googlelogin.GoogleLoginServiceBlockingHelper;
+import com.google.android.googlelogin.GoogleLoginServiceConstants;
+import com.google.android.googlelogin.GoogleLoginServiceNotFoundException;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import android.net.http.AndroidHttpClient;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpGet;
+import com.android.internal.http.multipart.StringPart;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.util.EncodingUtils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class UploadService extends Service implements Runnable {
+ private static final String TAG = "UploadService";
+
+ static final boolean DEBUG = false;
+ private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ private GoogleLoginServiceBlockingHelper mGls;
+ static public final int MSG_STATUS = 3;
+ static public final int EVENT_UPLOAD_ERROR = 400;
+
+ static public final String sPicasaService = "lh2";
+ static public final String sYouTubeService = "youtube";
+ static public final String sYouTubeUserService = "YouTubeUser";
+
+ static public final String sUploadAlbumName = "android_upload";
+ HashMap<String, Album> mAlbums;
+ ArrayList<String> mAndroidUploadAlbumPhotos = null;
+ HashMap<String, String> mGDataAuthTokenMap = new HashMap<String, String>();
+
+ int mStartId;
+ Thread mThread;
+
+ android.os.Handler mHandler = new android.os.Handler() {
+
+ };
+
+ ArrayList<Runnable> mStatusListeners = new ArrayList<Runnable>();
+
+ ArrayList<Uri> mUploadList = new ArrayList<Uri>();
+
+ ImageManager.IImageList mImageList = null;
+
+ String mPicasaUsername;
+ String mPicasaAuthToken;
+ String mYouTubeUsername;
+ String mYouTubeAuthToken;
+
+ AndroidHttpClient mClient = AndroidHttpClient.newInstance("Android-Camera/0.1");
+
+ private static final ComponentName sLogin = new ComponentName(
+ "com.google.android.googleapps",
+ "com.google.android.googleapps.GoogleLoginService");
+
+ public UploadService() {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "UploadService Constructor !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ }
+
+ private void computeAuthToken() {
+ if (LOCAL_LOGV) Log.v(TAG, "computeAuthToken()");
+ if (mPicasaAuthToken != null) return;
+
+ try {
+ String account = mGls.getAccount(GoogleLoginServiceConstants.REQUIRE_GOOGLE);
+ GoogleLoginCredentialsResult result =
+ mGls.getCredentials(account, sPicasaService, true);
+ mPicasaAuthToken = result.getCredentialsString();
+ mPicasaUsername = result.getAccount();
+ if (Config.LOGV)
+ Log.v(TAG, "mPicasaUsername is " + mPicasaUsername);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not get auth token", e);
+ }
+ }
+
+ private void computeYouTubeAuthToken() {
+ if (LOCAL_LOGV) Log.v(TAG, "computeYouTubeAuthToken()");
+ if (mYouTubeAuthToken != null) return;
+
+ try {
+ String account = mGls.getAccount(GoogleLoginServiceConstants.REQUIRE_GOOGLE);
+ GoogleLoginCredentialsResult result =
+ mGls.getCredentials(account, sYouTubeService, true);
+ mYouTubeAuthToken = result.getCredentialsString();
+ mYouTubeUsername = result.getAccount();
+ if (mYouTubeAuthToken.equals("NoLinkedYouTubeAccount")) {
+ // we successfully logged in to the google account, but it
+ // is not linked to a YouTube username.
+ if (Config.LOGV)
+ Log.v(TAG, "account " + mYouTubeUsername + " is not linked to a youtube account");
+ mYouTubeAuthToken = null;
+ return;
+ }
+
+ mYouTubeUsername = mGls.peekCredentials(mYouTubeUsername, sYouTubeUserService);
+ // now mYouTubeUsername is the YouTube username linked to the
+ // google account, which is probably what we want to display.
+
+ if (Config.LOGV)
+ Log.v(TAG, "3 mYouTubeUsername: " + mYouTubeUsername);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not get auth token", e);
+ }
+ }
+
+ NotificationManager mNotificationManager;
+
+ @Override
+ public void onCreate() {
+
+ try {
+ mGls = new GoogleLoginServiceBlockingHelper(this);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not find google login service, stopping service");
+ stopSelf();
+ }
+
+ if (mThread == null) {
+ mThread = new Thread(this);
+ mThread.start();
+ }
+ mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
+
+ IntentFilter intentFilter = new IntentFilter("com.android.camera.NEW_PICTURE");
+ b = new android.content.BroadcastReceiver() {
+ public void onReceive(android.content.Context ctx, Intent intent) {
+ android.content.SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ if (prefs.getBoolean("pref_camera_autoupload_key", false)) {
+ if (Config.LOGV)
+ Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> auto upload " + intent.getData());
+ }
+ }
+ };
+ registerReceiver(b, intentFilter);
+ }
+
+ android.content.BroadcastReceiver b = null;
+
+ @Override
+ public void onDestroy() {
+ mGls.close();
+ if (b != null) {
+ unregisterReceiver(b);
+ }
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "UploadService.onStart; this is " + hashCode());
+
+ if (mImageList == null) {
+ mImageList = ImageManager.instance().allImages(
+ this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_ASCENDING);
+ mImageList.setOnChangeListener(new ImageManager.IImageList.OnChange() {
+ public void onChange(ImageManager.IImageList list) {
+ /*
+ Log.v(TAG, "onChange <<<<<<<<<<<<<<<<<<<<<<<<<");
+ for (int i = 0; i < list.getCount(); i++) {
+ ImageManager.IImage img = list.getImageAt(i);
+ Log.v(TAG, "pos " + i + " " + img.fullSizeImageUri());
+ String picasaId = img.getPicasaId();
+ if (picasaId == null || picasaId.length() == 0) {
+ synchronized (mUploadList) {
+ Uri uri = img.fullSizeImageUri();
+ if (mUploadList.contains(uri)) {
+ mUploadList.add(img.fullSizeImageUri());
+ mUploadList.notify();
+ }
+ }
+ }
+ }
+ */
+ }
+ }, mHandler);
+ }
+
+ if (LOCAL_LOGV)
+ Log.v(TAG, "got image list with count " + mImageList.getCount());
+
+ synchronized (mUploadList) {
+ mStartId = startId;
+ String uriString = intent.getStringExtra("imageuri");
+
+ if (LOCAL_LOGV)
+ Log.v(TAG, "starting UploadService; startId = " + startId + " start uri: " + uriString);
+
+ if (uriString != null) {
+ Uri uri = Uri.parse(uriString);
+ IImage image = mImageList.getImageForUri(uri);
+ if (!mUploadList.contains(uri)) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "queing upload of " + image.fullSizeImageUri());
+ mUploadList.add(uri);
+ }
+ } else {
+ // for now upload all applies to images only, not videos
+ for (int i = 0; i < mImageList.getCount(); i++) {
+ IImage image = mImageList.getImageAt(i);
+ if (image instanceof ImageManager.Image) {
+ Uri uri = image.fullSizeImageUri();
+ if (!mUploadList.contains(uri)) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "queing upload of " + image.fullSizeImageUri());
+ mUploadList.add(uri);
+ }
+ }
+ }
+ }
+ updateNotification();
+ }
+
+ synchronized(mUploadList) {
+ mUploadList.notify();
+ }
+ }
+
+ void updateNotification() {
+ int videosCount = 0, imagesCount = 0;
+ for (int i = 0;i < mUploadList.size(); i++) {
+ // TODO yes this is a hack
+ Uri uri = mUploadList.get(i);
+ if (uri.toString().contains("video"))
+ videosCount += 1;
+ else
+ imagesCount += 1;
+ }
+ updateNotification(imagesCount, videosCount);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ // This is the object that recieves interactions from clients.
+ private final IBinder mBinder = new Binder() {
+ protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
+ return true;
+ }
+ };
+
+ private void updateNotification(int pendingImagesCount, int pendingVideosCount) {
+ final int mVideoUploadId = 1;
+ final int mImageUploadId = 2;
+ if (pendingImagesCount == 0) {
+ if (mNotificationManager != null)
+ mNotificationManager.cancel(mImageUploadId);
+ } else {
+ String detailedMsg = String.format(getResources().getString(R.string.uploadingNPhotos), pendingImagesCount);
+ Notification n = new Notification(
+ this,
+ android.R.drawable.stat_sys_upload,
+ getResources().getString(R.string.uploading_photos),
+ System.currentTimeMillis(),
+ getResources().getString(R.string.uploading_photos_2),
+ detailedMsg,
+ null);
+ mNotificationManager.notify(mImageUploadId, n);
+ }
+ if (pendingVideosCount == 0) {
+ if (mNotificationManager != null)
+ mNotificationManager.cancel(mVideoUploadId);
+ } else {
+ String detailedMsg = String.format(getResources().getString(R.string.uploadingNVideos), pendingImagesCount);
+ Notification n = new Notification(
+ this,
+ android.R.drawable.stat_sys_upload,
+ getResources().getString(R.string.uploading_videos),
+ System.currentTimeMillis(),
+ getResources().getString(R.string.uploading_videos_2),
+ detailedMsg,
+ null);
+ mNotificationManager.notify(mVideoUploadId, n);
+ }
+ }
+
+ public void run() {
+ try {
+ if (Config.LOGV)
+ Log.v(TAG, "running upload thread...");
+ while (true) {
+ IImage image = null;
+ synchronized (mUploadList) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "mUploadList.size() is " + mUploadList.size());
+ if (mUploadList.size() == 0) {
+ try {
+ updateNotification(0, 0);
+ if (Config.LOGV)
+ Log.v(TAG, "waiting...");
+ mUploadList.wait(60000);
+ if (Config.LOGV)
+ Log.v(TAG, "done waiting...");
+ } catch (InterruptedException ex) {
+ }
+ if (mUploadList.size() == 0) {
+// if (LOCAL_LOGV) Log.v(TAG, "exiting run, stoping service");
+// stopSelf(mStartId);
+// break;
+ continue;
+ }
+ }
+ Uri uri = mUploadList.get(0);
+ image = mImageList.getImageForUri(uri);
+ if (Config.LOGV)
+ Log.v(TAG, "got uri " + uri + " " + image);
+ }
+
+ boolean success = false;
+ if (image != null) {
+ updateNotification();
+
+ long t1 = System.currentTimeMillis();
+ success = uploadItem(image);
+ long t2 = System.currentTimeMillis();
+ if (LOCAL_LOGV) Log.v(TAG, "upload took " + (t2-t1) + "; success = " + success);
+ }
+
+ synchronized (mUploadList) {
+ mUploadList.remove(0);
+ if (!success && image != null) {
+ mUploadList.add(image.fullSizeImageUri());
+ }
+ }
+ if (!success) {
+ int retryDelay = 30000;
+ if (LOCAL_LOGV)
+ Log.v(TAG, "failed to upload " + image.fullSizeImageUri() + " trying again in " + retryDelay + " ms");
+ try {
+ synchronized (mUploadList) {
+ long t1x = System.currentTimeMillis();
+ mUploadList.wait(retryDelay);
+ long t2x = System.currentTimeMillis();
+ if (Config.LOGV)
+ Log.v(TAG, "retry waited " + (t2x-t1x));
+ }
+ } catch (InterruptedException ex) {
+ if (Config.LOGV)
+ Log.v(TAG, "ping, was waiting but now retry again");
+ };
+ }
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception in upload thread", ex);
+ }
+ finally {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "finished task");
+ }
+ }
+
+ private String getLatLongString(IImage image) {
+ if (image.hasLatLong()) {
+ return "<georss:where><gml:Point><gml:pos>"
+ + image.getLatitude()
+ + " "
+ + image.getLongitude()
+ + "</gml:pos></gml:Point></georss:where>";
+ } else {
+ return "";
+ }
+ }
+
+ private String uploadAlbumName() {
+ android.content.SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String s = prefs.getString("pref_camera_upload_albumname_key", sUploadAlbumName);
+ return s;
+ }
+
+ private boolean uploadItem(IImage image) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "starting work on " + image);
+
+ if (image instanceof ImageManager.VideoObject) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "Uploading video");
+ computeYouTubeAuthToken();
+ return (new VideoUploadTask(image)).upload();
+ } else {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "Uploading photo");
+
+ computeAuthToken();
+ // handle photos
+ if (mAlbums == null)
+ mAlbums = getAlbums();
+
+ String albumName = uploadAlbumName();
+ if (mAlbums == null || !mAlbums.containsKey(albumName)) {
+ Album a = createAlbum(albumName, uploadAlbumName());
+ if (a == null) {
+ return false;
+ }
+ if (LOCAL_LOGV)
+ Log.v(TAG, "made new album: " + a.getAlbumName() + "; " + a.getAlbumId());
+ mAlbums.put(a.getAlbumName(), a);
+ }
+
+ if (mAndroidUploadAlbumPhotos == null)
+ mAndroidUploadAlbumPhotos = getAlbumContents(albumName);
+
+ if (mAndroidUploadAlbumPhotos != null) {
+ String previousUploadId = image.getPicasaId();
+ if (previousUploadId != null) {
+ if (mAndroidUploadAlbumPhotos.contains(previousUploadId)) {
+ if (Config.LOGV)
+ Log.v(TAG, "already have id " + previousUploadId);
+ return true;
+ }
+ }
+ }
+ Album album = mAlbums.get(albumName);
+ return (new ImageUploadTask(image)).upload(album);
+ }
+ }
+
+// void broadcastError(int error) {
+// HashMap map = new HashMap();
+// map.put("error", new Integer(error));
+//
+// Message send = Message.obtain();
+// send.what = EVENT_UPLOAD_ERROR;
+// send.setData(map);
+//
+// if (mBroadcaster == null) {
+// mBroadcaster = new Broadcaster();
+// }
+// mBroadcaster.broadcast(send);
+// }
+
+ class Album {
+ String mAlbumName;
+
+ String mAlbumId;
+
+ public Album() {
+ }
+
+ public void setAlbumName(String albumName) {
+ mAlbumName = albumName;
+ }
+
+ public void setAlbumId(String albumId) {
+ mAlbumId = albumId;
+ }
+
+ public String getAlbumName() {
+ return mAlbumName;
+ }
+
+ public String getAlbumId() {
+ return mAlbumId;
+ }
+ }
+
+ static private String stringFromResponse(HttpResponse response) {
+ try {
+ HttpEntity entity = response.getEntity();
+ InputStream inputStream = entity.getContent();
+ StringWriter s = new StringWriter();
+ while (true) {
+ int c = inputStream.read();
+ if (c == -1)
+ break;
+ s.write((char)c);
+ }
+ inputStream.close();
+ String retval = s.toString();
+ if (Config.LOGV)
+ Log.v(TAG, "got resposne " + retval);
+ return retval;
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ abstract class UploadTask {
+ IImage mImageObj;
+
+ public UploadTask(IImage image) {
+ mImageObj = image;
+ }
+
+ public class UploadResponse {
+ private HttpResponse mStatus;
+ private String mBody;
+
+ public UploadResponse(HttpResponse status) {
+ mStatus = status;
+ mBody = stringFromResponse(status);
+ }
+
+ public int getStatus() {
+ return mStatus.getStatusLine().getStatusCode();
+ }
+
+ public String getResponse() {
+ return mBody;
+ }
+ }
+
+ class StreamPart extends PartBase {
+ InputStream mInputStream;
+ long mLength;
+
+ StreamPart(String name, InputStream inputStream, String contentType) {
+ super(name,
+ contentType == null ? "application/octet-stream" : contentType,
+ "ISO-8859-1",
+ "binary"
+ );
+ mInputStream = inputStream;
+ try {
+ mLength = inputStream.available();
+ } catch (IOException ex) {
+
+ }
+ }
+
+ @Override
+ protected long lengthOfData() throws IOException {
+ return mLength;
+ }
+
+ @Override
+ protected void sendData(OutputStream out) throws IOException {
+ byte [] buffer = new byte[4096];
+ while (true) {
+ int got = mInputStream.read(buffer);
+ if (got == -1)
+ break;
+ out.write(buffer, 0, got);
+ }
+ mInputStream.close();
+ }
+
+ @Override
+ protected void sendDispositionHeader(OutputStream out) throws IOException {
+ }
+
+ @Override
+ protected void sendContentTypeHeader(OutputStream out) throws IOException {
+ String contentType = getContentType();
+ if (contentType != null) {
+ out.write(CONTENT_TYPE_BYTES);
+ out.write(EncodingUtils.getAsciiBytes(contentType));
+ String charSet = getCharSet();
+ if (charSet != null) {
+ out.write(CHARSET_BYTES);
+ out.write(EncodingUtils.getAsciiBytes(charSet));
+ }
+ }
+ }
+ }
+
+ public class StringPartX extends StringPart {
+ public StringPartX(String name, String value, String charset) {
+ super(name, value, charset);
+ setContentType("application/atom+xml");
+ }
+
+ @Override
+ protected void sendDispositionHeader(OutputStream out) throws IOException {
+ }
+
+ @Override
+ protected void sendContentTypeHeader(OutputStream out) throws IOException {
+ String contentType = getContentType();
+ if (contentType != null) {
+ out.write(CONTENT_TYPE_BYTES);
+ out.write(EncodingUtils.getAsciiBytes(contentType));
+ String charSet = getCharSet();
+ if (charSet != null) {
+ out.write(CHARSET_BYTES);
+ out.write(EncodingUtils.getAsciiBytes(charSet));
+ }
+ }
+ }
+ }
+
+ public class MultipartEntityX extends MultipartEntity {
+ public MultipartEntityX(Part[] parts, HttpParams params) {
+ super(parts, params);
+ }
+
+ @Override
+ public Header getContentType() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("multipart/related; boundary=");
+ buffer.append(EncodingUtils.getAsciiString(getMultipartBoundary()));
+ return new BasicHeader(HTTP.CONTENT_TYPE, buffer.toString());
+ }
+
+ }
+
+ protected UploadResponse doUpload(String uploadUrl,
+ String mimeType,
+ String data,
+ IImage imageObj,
+ String authToken,
+ String title,
+ String filename,
+ boolean youTubeAuthenticate) {
+ if (authToken == null)
+ return null;
+
+ FileInputStream inputStream = (FileInputStream)mImageObj.fullSizeImageData();
+ try {
+ HttpPost post = new HttpPost(uploadUrl);
+ post.addHeader(new BasicHeader("Authorization", "GoogleLogin auth=" + authToken));
+ if (youTubeAuthenticate) {
+ // TODO: remove hardwired key? - This is our official YouTube issued developer key to Android.
+ String youTubeDeveloperKey = "key=AI39si5Cr35CiD1IgDqD9Ua6N4dSbY-oibnLUPITmBN_rFW6qRz-hd8sTqNzRf1gzNwSYZbDuS31Txa4iKyjAV77507O4tq7JA";
+ post.addHeader("X-GData-Key", youTubeDeveloperKey);
+ post.addHeader("Slug", filename);
+ }
+
+ Part p1 = new StringPartX("param_name", data, null);
+ Part p2 = new StreamPart("field_uploadfile", inputStream, mimeType);
+
+ MultipartEntity mpe = new MultipartEntityX(new Part[] { p1, p2 }, post.getParams());
+ post.setEntity(mpe);
+ HttpResponse status = mClient.execute(post);
+ if (LOCAL_LOGV) Log.v(TAG, "doUpload response is " + status.getStatusLine());
+ return new UploadResponse(status);
+ } catch (java.io.IOException ex) {
+ if (LOCAL_LOGV) Log.v(TAG, "IOException in doUpload", ex);
+ return null;
+ }
+ }
+
+ class ResponseHandler implements ElementListener {
+ private static final String ATOM_NAMESPACE
+ = "http://www.w3.org/2005/Atom";
+ private static final String PICASSA_NAMESPACE
+ = "http://schemas.google.com/photos/2007";
+
+ private ContentHandler mHandler = null;
+ private String mId = null;
+
+ public ResponseHandler() {
+ RootElement root = new RootElement(ATOM_NAMESPACE, "entry");
+ Element entry = root;
+ entry.setElementListener(this);
+
+ entry.getChild(PICASSA_NAMESPACE, "id")
+ .setEndTextElementListener(new EndTextElementListener() {
+ public void end(String body) {
+ mId = body;
+ }
+ });
+
+ mHandler = root.getContentHandler();
+ }
+
+ public void start(Attributes attributes) {
+ }
+
+ public void end() {
+ }
+
+ ContentHandler getContentHandler() {
+ return mHandler;
+ }
+
+ public String getId() {
+ return mId;
+ }
+ }
+ }
+
+ private class VideoUploadTask extends UploadTask {
+ public VideoUploadTask(IImage image) {
+ super(image);
+ }
+ protected String getYouTubeBaseUrl() {
+ return "http://gdata.youtube.com";
+ }
+
+ public boolean upload() {
+ String uploadUrl = "http://uploads.gdata.youtube.com"
+ + "/feeds/users/"
+ + mYouTubeUsername
+ + "/uploads?client=ytapi-google-android";
+
+ String title = mImageObj.getTitle();
+ String isPrivate = "";
+ String keywords = "";
+ String category = "";
+ if (mImageObj instanceof ImageManager.VideoObject) {
+ ImageManager.VideoObject video = (ImageManager.VideoObject)mImageObj;
+ if (mImageObj.getIsPrivate()) {
+ isPrivate = "<yt:private/>";
+ }
+ keywords = video.getTags();
+ if (keywords == null || keywords.trim().length() == 0) {
+ // there must be a keyword or YouTube will reject the video
+ keywords = getResources().getString(R.string.upload_default_tags_text);
+ }
+ // TODO: use the real category when we have the category spinner in details
+// category = video.getCategory();
+ category = "";
+ if (category == null || category.trim().length() == 0) {
+ // there must be a description or YouTube will get an internal error and return 500
+ category = getResources().getString(R.string.upload_default_category_text);
+ }
+ }
+ String description = mImageObj.getDescription();
+ if (description == null || description.trim().length() == 0) {
+ // there must be a description or YouTube will get an internal error and return 500
+ description = getResources().getString(R.string.upload_default_description_text);
+ }
+ String data = "<?xml version='1.0'?>\n"
+ + "<entry xmlns='http://www.w3.org/2005/Atom'\n"
+ + " xmlns:media='http://search.yahoo.com/mrss/'\n"
+ + " xmlns:yt='http://gdata.youtube.com/schemas/2007'>\n"
+ + " <media:group>\n"
+ + " <media:title type='plain'>" + title + "</media:title>\n" // TODO: need user entered title
+ + " <media:description type='plain'>" + description + "</media:description>\n"
+ + isPrivate
+ + " <media:category scheme='http://gdata.youtube.com/schemas/2007/categories.cat'>\n"
+ + category
+ + " </media:category>\n"
+ + " <media:keywords>" + keywords + "</media:keywords>\n"
+ + " </media:group>\n"
+ + "</entry>";
+
+ if (LOCAL_LOGV) Log.v("youtube", "uploadUrl: " + uploadUrl);
+ if (LOCAL_LOGV) Log.v("youtube", "GData: " + data);
+
+ UploadResponse result = doUpload(uploadUrl,
+ "video/3gpp2",
+ data,
+ null,
+ mYouTubeAuthToken,
+ title,
+ mImageObj.fullSizeImageUri().getLastPathSegment(),
+ true);
+
+ boolean success = false;
+ if (result != null) {
+ switch (result.getStatus()) {
+ case 401:
+ if (result.getResponse().contains("Token expired")) {
+ // When we tried to upload a video to YouTube, the youtube server told us
+ // our auth token was expired. Get a new one and try again.
+ try {
+ mGls.invalidateAuthToken(mYouTubeAuthToken);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not invalidate youtube auth token", e);
+ }
+ mYouTubeAuthToken = null; // Forces computeYouTubeAuthToken to get a new token.
+ computeYouTubeAuthToken();
+ }
+ break;
+
+ case 200:
+ case 201:
+ case 202:
+ case 203:
+ case 204:
+ case 205:
+ case 206:
+ success = true;
+ break;
+
+ }
+ }
+ return success;
+ }
+ }
+
+ private class ImageUploadTask extends UploadTask {
+ public ImageUploadTask(IImage image) {
+ super(image);
+ }
+
+ public boolean upload(Album album) {
+ String uploadUrl = getServiceBaseUrl()
+ + mPicasaUsername
+ + "/album/"
+ + album.getAlbumId();
+
+ String name = mImageObj.getTitle();
+ String description = mImageObj.getDescription();
+ String data = "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:georss='http://www.georss.org/georss' xmlns:gml='http://www.opengis.net/gml'><title>"
+ + name
+ + "</title>"
+ + "<summary>"
+ + (description != null ? description : "")
+ + "</summary>"
+ + getLatLongString(mImageObj)
+ + "<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/photos/2007#photo'/></entry>\n";
+
+ if (LOCAL_LOGV)
+ Log.v(TAG, "xml for image is " + data);
+ UploadResponse response = doUpload(uploadUrl,
+ "image/jpeg",
+ data,
+ mImageObj,
+ mPicasaAuthToken,
+ name,
+ name,
+ false);
+
+ if (response != null) {
+ int status = response.getStatus();
+ if (status == HttpStatus.SC_UNAUTHORIZED ||
+ status == HttpStatus.SC_FORBIDDEN ||
+ status == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
+ try {
+ mGls.invalidateAuthToken(mPicasaAuthToken);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not invalidate picasa auth token", e);
+ }
+ mPicasaAuthToken = null;
+ } else {
+ ResponseHandler h = new ResponseHandler();
+ try {
+ Xml.parse(response.getResponse(), h.getContentHandler());
+ String id = h.getId();
+ if (id != null && mImageObj != null) {
+ mImageObj.setPicasaId(id);
+ mAndroidUploadAlbumPhotos.add(id);
+ return true;
+ }
+ } catch (org.xml.sax.SAXException ex) {
+ Log.e(TAG, "SAXException in doUpload " + ex.toString());
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ private Album createAlbum(String name, String summary) {
+ String authToken = mPicasaAuthToken;
+ if (authToken == null)
+ return null;
+
+ try {
+ String url = getServiceBaseUrl() + mPicasaUsername;
+ HttpPost post = new HttpPost(url);
+ String entryString = "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:media='http://search.yahoo.com/mrss/' xmlns:gphoto='http://schemas.google.com/photos/2007'>"
+ + "<title type='text'>"
+ + name
+ + "</title>"
+ + "<summary>"
+ + summary
+ + "</summary>"
+ + "<gphoto:access>private</gphoto:access>"
+ + "<gphoto:commentingEnabled>true</gphoto:commentingEnabled>"
+ + "<gphoto:timestamp>"
+ + String.valueOf(System.currentTimeMillis())
+ + "</gphoto:timestamp>"
+ + "<category scheme=\"http://schemas.google.com/g/2005#kind\" term=\"http://schemas.google.com/photos/2007#album\"/>"
+ + "</entry>\n";
+
+ StringEntity entity = new StringEntity(entryString);
+ entity.setContentType(new BasicHeader("Content-Type", "application/atom+xml"));
+ post.setEntity(entity);
+ post.addHeader(new BasicHeader("Authorization", "GoogleLogin auth=" + authToken));
+ HttpResponse status = mClient.execute(post);
+ if (LOCAL_LOGV)
+ Log.v(TAG, "status is " + status.getStatusLine());
+ if (status.getStatusLine().getStatusCode() < 200 || status.getStatusLine().getStatusCode() >= 300) {
+ return null;
+ }
+ Album album = new Album();
+ Xml.parse(stringFromResponse(status), new PicasaAlbumHandler(album).getContentHandler());
+ return album;
+ } catch (java.io.UnsupportedEncodingException ex) {
+ Log.e(TAG, "gak, UnsupportedEncodingException " + ex.toString());
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "IOException " + ex.toString());
+ } catch (org.xml.sax.SAXException ex) {
+ Log.e(TAG, "XmlPullParserException " + ex.toString());
+ }
+ return null;
+ }
+
+ public static String streamToString(InputStream stream, int maxChars, boolean reset)
+ throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream), 8192);
+ StringBuilder sb = new StringBuilder();
+ String line = null;
+
+ while ((line = reader.readLine()) != null
+ && (maxChars == -1 || sb.length() < maxChars)) {
+ sb.append(line);
+ }
+ reader.close();
+ if (reset) stream.reset();
+ return sb.toString();
+ }
+
+ InputStream get(String url) {
+ try {
+ if (LOCAL_LOGV) Log.v(TAG, "url is " + url);
+
+ for (int i = 0; i < 2; ++i) {
+ HttpGet get = new HttpGet(url);
+ get.setHeader(new BasicHeader("Authorization",
+ "GoogleLogin auth=" + mPicasaAuthToken));
+
+ HttpResponse response = mClient.execute(get);
+ if (LOCAL_LOGV) Log.v(TAG, "response is " + response.getStatusLine());
+ switch (response.getStatusLine().getStatusCode()) {
+ case HttpStatus.SC_UNAUTHORIZED:
+ case HttpStatus.SC_FORBIDDEN:
+ case HttpStatus.SC_INTERNAL_SERVER_ERROR: // http://b/1151576
+ try {
+ mGls.invalidateAuthToken(mPicasaAuthToken);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not invalidate picasa auth token", e);
+ }
+ mPicasaAuthToken = null;
+ computeAuthToken();
+ if (mPicasaAuthToken != null) {
+ // retry fetch after getting new token
+ continue;
+ }
+ break;
+ }
+
+ InputStream inputStream = response.getEntity().getContent();
+ return inputStream;
+ }
+ return null;
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "IOException");
+ }
+ return null;
+ }
+
+ private HashMap<String, Album> getAlbums() {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "getAlbums");
+
+ PicasaAlbumHandler h = new PicasaAlbumHandler();
+ try {
+ String url = getServiceBaseUrl() + mPicasaUsername + "?kind=album";
+ InputStream inputStream = get(url);
+ if (inputStream == null) {
+ if (Config.LOGV)
+ Log.v(TAG, "can't get " + url + "; bail from getAlbums()");
+ mPicasaAuthToken = null;
+ return null;
+ }
+
+ Xml.parse(inputStream, Xml.findEncodingByName("UTF-8"), h.getContentHandler());
+ if (LOCAL_LOGV)
+ Log.v(TAG, "done getting albums");
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(TAG, "got exception " + e.toString());
+ e.printStackTrace();
+ } catch (SAXException e) {
+ Log.e(TAG, "got exception " + e.toString());
+ e.printStackTrace();
+ }
+ if (LOCAL_LOGV) {
+ java.util.Iterator it = h.getAlbums().keySet().iterator();
+ while (it.hasNext()) {
+ if (Config.LOGV)
+ Log.v(TAG, "album: " + (String) it.next());
+ }
+ }
+ return h.getAlbums();
+ }
+
+ ArrayList<String> getAlbumContents(String albumName) {
+ String url = getServiceBaseUrl() + mPicasaUsername + "/album/" + albumName + "?kind=photo&max-results=10000";
+ try {
+ InputStream inputStream = get(url);
+ if (inputStream == null)
+ return null;
+
+ AlbumContentsHandler ah = new AlbumContentsHandler();
+ Xml.parse(inputStream, Xml.findEncodingByName("UTF-8"), ah.getContentHandler());
+ ArrayList<String> photos = ah.getPhotos();
+ inputStream.close();
+ return photos;
+ } catch (IOException e) {
+ Log.e(TAG, "got IOException " + e.toString());
+ e.printStackTrace();
+ } catch (SAXException e) {
+ Log.e(TAG, "got SAXException " + e.toString());
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ class AlbumContentsHandler implements ElementListener {
+ private static final String ATOM_NAMESPACE
+ = "http://www.w3.org/2005/Atom";
+ private static final String PICASA_NAMESPACE
+ = "http://schemas.google.com/photos/2007";
+
+ private ContentHandler mHandler = null;
+ private ArrayList<String> mPhotos = new ArrayList<String>();
+
+ public AlbumContentsHandler() {
+ RootElement root = new RootElement(ATOM_NAMESPACE, "feed");
+ Element entry = root.getChild(ATOM_NAMESPACE, "entry");
+
+ entry.setElementListener(this);
+
+ entry.getChild(PICASA_NAMESPACE, "id")
+ .setEndTextElementListener(new EndTextElementListener() {
+ public void end(String body) {
+ mPhotos.add(body);
+ }
+ });
+
+ mHandler = root.getContentHandler();
+ }
+
+ public void start(Attributes attributes) {
+ }
+
+ public void end() {
+ }
+
+ ContentHandler getContentHandler() {
+ return mHandler;
+ }
+
+ public ArrayList<String> getPhotos() {
+ return mPhotos;
+ }
+ }
+
+ private String getServiceBaseUrl() {
+ return "http://picasaweb.google.com/data/feed/api/user/";
+ }
+
+
+ class PicasaAlbumHandler implements ElementListener {
+ private Album mAlbum;
+ private HashMap<String, Album> mAlbums = new HashMap<String, Album>();
+ private boolean mJustOne;
+ private static final String ATOM_NAMESPACE
+ = "http://www.w3.org/2005/Atom";
+ private static final String PICASSA_NAMESPACE
+ = "http://schemas.google.com/photos/2007";
+ private ContentHandler handler = null;
+
+ public PicasaAlbumHandler() {
+ mJustOne = false;
+ init();
+ }
+
+ public HashMap<String, Album> getAlbums() {
+ return mAlbums;
+ }
+
+ public PicasaAlbumHandler(Album album) {
+ mJustOne = true;
+ mAlbum = album;
+ init();
+ }
+
+ private void init() {
+ Element entry;
+ RootElement root;
+ if (mJustOne) {
+ root = new RootElement(ATOM_NAMESPACE, "entry");
+ entry = root;
+ } else {
+ root = new RootElement(ATOM_NAMESPACE, "feed");
+ entry = root.getChild(ATOM_NAMESPACE, "entry");
+ }
+ entry.setElementListener(this);
+
+ entry.getChild(ATOM_NAMESPACE, "title")
+ .setEndTextElementListener(new EndTextElementListener() {
+ public void end(String body) {
+ mAlbum.setAlbumName(body);
+ }
+ });
+
+ entry.getChild(PICASSA_NAMESPACE, "name")
+ .setEndTextElementListener(new EndTextElementListener() {
+ public void end(String body) {
+ mAlbum.setAlbumId(body);
+ }
+ });
+
+ this.handler = root.getContentHandler();
+ }
+
+ public void start(Attributes attributes) {
+ if (!mJustOne) {
+ mAlbum = new Album();
+ }
+ }
+
+ public void end() {
+ if (!mJustOne) {
+ mAlbums.put(mAlbum.getAlbumName(), mAlbum);
+ mAlbum = null;
+ }
+ }
+
+ ContentHandler getContentHandler() {
+ return handler;
+ }
+ }
+}
diff --git a/src/com/android/camera/ViewImage.java b/src/com/android/camera/ViewImage.java
new file mode 100644
index 0000000..4b9eb58
--- /dev/null
+++ b/src/com/android/camera/ViewImage.java
@@ -0,0 +1,1434 @@
+/*
+ * Copyright (C) 5163 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.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+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.os.PowerManager;
+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.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.Animation;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.AnimationUtils;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.Scroller;
+import android.widget.TextView;
+import android.widget.ZoomControls;
+import android.preference.PreferenceManager;
+
+import com.android.camera.ImageManager.IImage;
+
+import java.util.Random;
+
+public class ViewImage extends Activity
+{
+ 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 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 MenuItem mFlipItem;
+
+ 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;
+
+ Runnable mDismissOnScreenControlsRunnable;
+ ZoomControls mZoomControls;
+
+ 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() {
+ if (mZoomControls != null) {
+ if (mZoomControls.getVisibility() == View.GONE) {
+ mZoomControls.show();
+ mZoomControls.requestFocus(); // this shouldn't be necessary
+ }
+ 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 View getZoomControls() {
+ if (mZoomControls == null) {
+ mZoomControls = new ZoomControls(this);
+ mZoomControls.setVisibility(View.GONE);
+ mZoomControls.setZoomSpeed(0);
+ mDismissOnScreenControlsRunnable = new Runnable() {
+ public void run() {
+ mZoomControls.hide();
+
+ 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);
+ }
+ }
+ };
+ mZoomControls.setOnZoomInClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
+ mImageViews[1].zoomIn();
+ scheduleDismissOnScreenControls();
+ }
+ });
+ mZoomControls.setOnZoomOutClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
+ mImageViews[1].zoomOut();
+ scheduleDismissOnScreenControls();
+ }
+ });
+ }
+ return mZoomControls;
+ }
+
+ private boolean isPickIntent() {
+ String action = getIntent().getAction();
+ return (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action));
+ }
+
+ private static final boolean sDragLeftRight = false;
+ private static final boolean sUseBounce = false;
+ private static final boolean sAnimateTransitions = false;
+
+ static public class ImageViewTouch extends ImageViewTouchBase {
+ private ViewImage mViewImage;
+
+ private static int TOUCH_STATE_REST = 0;
+ private static int TOUCH_STATE_LEFT_PRESS = 1;
+ private static int TOUCH_STATE_RIGHT_PRESS = 2;
+ private static int TOUCH_STATE_PANNING = 3;
+
+ private static int TOUCH_AREA_WIDTH = 60;
+
+ private int mTouchState = TOUCH_STATE_REST;
+
+ public ImageViewTouch(Context context) {
+ super(context);
+ mViewImage = (ViewImage) context;
+ }
+
+ public ImageViewTouch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mViewImage = (ViewImage) context;
+ }
+
+ 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) {
+ int viewWidth = getWidth();
+ ViewImage viewImage = mViewImage;
+ int x = (int) m.getX();
+ int y = (int) m.getY();
+
+ switch (m.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ viewImage.setMode(MODE_NORMAL);
+ viewImage.showOnScreenControls();
+ mLastXTouchPos = x;
+ mLastYTouchPos = y;
+ mTouchState = TOUCH_STATE_REST;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (x < TOUCH_AREA_WIDTH) {
+ if (mTouchState == TOUCH_STATE_REST) {
+ mTouchState = TOUCH_STATE_LEFT_PRESS;
+ }
+ if (mTouchState == TOUCH_STATE_LEFT_PRESS) {
+ viewImage.mPrevImageView.setPressed(true);
+ viewImage.mNextImageView.setPressed(false);
+ }
+ mLastXTouchPos = x;
+ mLastYTouchPos = y;
+ } else if (x > viewWidth - TOUCH_AREA_WIDTH) {
+ if (mTouchState == TOUCH_STATE_REST) {
+ mTouchState = TOUCH_STATE_RIGHT_PRESS;
+ }
+ if (mTouchState == TOUCH_STATE_RIGHT_PRESS) {
+ viewImage.mPrevImageView.setPressed(false);
+ viewImage.mNextImageView.setPressed(true);
+ }
+ mLastXTouchPos = x;
+ mLastYTouchPos = y;
+ } else {
+ mTouchState = TOUCH_STATE_PANNING;
+ viewImage.mPrevImageView.setPressed(false);
+ viewImage.mNextImageView.setPressed(false);
+
+ int deltaX;
+ int deltaY;
+
+ if (mLastXTouchPos == -1) {
+ deltaX = 0;
+ deltaY = 0;
+ } else {
+ deltaX = x - mLastXTouchPos;
+ deltaY = y - mLastYTouchPos;
+ }
+
+ mLastXTouchPos = x;
+ mLastYTouchPos = y;
+
+ if (mBitmapDisplayed == null)
+ return true;
+
+ if (deltaX != 0) {
+ // Second. Pan to whatever degree is possible.
+ if (getScale() > 1F) {
+ postTranslate(deltaX, deltaY, sUseBounce);
+ ImageViewTouch.this.center(true, true, false);
+ }
+ }
+ setImageMatrix(getImageViewMatrix());
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ int nextImagePos = -1;
+ if (mTouchState == TOUCH_STATE_LEFT_PRESS && x < TOUCH_AREA_WIDTH) {
+ nextImagePos = viewImage.mCurrentPosition - 1;
+ } else if (mTouchState == TOUCH_STATE_RIGHT_PRESS &&
+ x > viewWidth - TOUCH_AREA_WIDTH) {
+ nextImagePos = viewImage.mCurrentPosition + 1;
+ }
+ if (nextImagePos >= 0
+ && nextImagePos < viewImage.mAllImages.getCount()) {
+ synchronized (viewImage) {
+ viewImage.setMode(MODE_NORMAL);
+ viewImage.setImage(nextImagePos);
+ }
+ }
+ viewImage.scheduleDismissOnScreenControls();
+ viewImage.mPrevImageView.setPressed(false);
+ viewImage.mNextImageView.setPressed(false);
+ mTouchState = TOUCH_STATE_REST;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ viewImage.mPrevImageView.setPressed(false);
+ viewImage.mNextImageView.setPressed(false);
+ mTouchState = TOUCH_STATE_REST;
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent 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();
+ }
+
+ }
+
+ 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 (true) {
+ 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);
+ }
+
+ mFlipItem = MenuHelper.addFlipOrientation(menu, ViewImage.this, mPrefs);
+ mFlipItem.setIcon(android.R.drawable.ic_menu_always_landscape_portrait);
+
+ 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,
+ 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));
+ }
+
+ int keyboard = getResources().getConfiguration().keyboardHidden;
+ mFlipItem.setEnabled(keyboard == android.content.res.Configuration.KEYBOARDHIDDEN_YES);
+
+ menu.findItem(MenuHelper.MENU_IMAGE_SHARE).setEnabled(isCurrentImageShareable());
+
+ return true;
+ }
+
+ private boolean isCurrentImageShareable() {
+ IImage image = mAllImages.getImageAt(mCurrentPosition);
+ if (image != null){
+ Uri uri = image.fullSizeImageUri();
+ String fullUri = uri.toString();
+ return fullUri.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString()) ||
+ fullUri.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString());
+ }
+ return true;
+ }
+
+ @Override
+ public void onConfigurationChanged(android.content.res.Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.setImageBitmapResetBase(null, false, true);
+ }
+ 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) {
+ 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);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ 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);
+
+ 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 (int i = 0; i < mSlideShowImageViews.length; i++) {
+ mSlideShowImageViews[i].setImageBitmapResetBase(null, true, true);
+ mSlideShowImageViews[i].setVisibility(View.INVISIBLE);
+ }
+
+ 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);
+ }
+
+ // Get the zoom controls and add them to the bottom of the map
+ View zoomControls = getZoomControls();
+ RelativeLayout root = (RelativeLayout) findViewById(R.id.rootLayout);
+ RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ p.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
+ p.addRule(RelativeLayout.CENTER_HORIZONTAL);
+ root.addView(zoomControls, p);
+
+ mNextImageView = findViewById(R.id.next_image);
+ mPrevImageView = findViewById(R.id.prev_image);
+
+ 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();
+ }
+
+ 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_FULLSCREEN
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ if (mGetter != null)
+ mGetter.cancelCurrent();
+
+ if (sSlideShowHidesStatusBar) {
+ win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+
+ 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 void init(Uri uri) {
+ String sortOrder = mPrefs.getString("pref_gallery_sort_key", null);
+ mSortAscending = false;
+ if (sortOrder != null) {
+ mSortAscending = sortOrder.equals("ascending");
+ }
+ 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();
+
+ ImageManager.IImage image = mAllImages.getImageAt(mCurrentPosition);
+
+ String sortOrder = mPrefs.getString("pref_gallery_sort_key", null);
+ boolean sortAscending = false;
+ if (sortOrder != null) {
+ sortAscending = sortOrder.equals("ascending");
+ }
+ if (sortAscending != 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);
+
+ // 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();
+ } else {
+ MenuHelper.requestOrientation(this, mPrefs);
+ }
+ }
+
+ @Override
+ public void onPause()
+ {
+ super.onPause();
+
+ mGetter.cancelCurrent();
+ mGetter.stop();
+ mGetter = null;
+ setMode(MODE_NORMAL);
+
+ mAllImages.deactivate();
+
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.recycleBitmaps();
+ iv.setImageBitmap(null, true);
+ }
+
+ for (ImageViewTouchBase iv: mSlideShowImageViews) {
+ iv.recycleBitmaps();
+ iv.setImageBitmap(null, true);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ }
+}
diff --git a/src/com/android/camera/ViewVideo.java b/src/com/android/camera/ViewVideo.java
new file mode 100644
index 0000000..527f0bb
--- /dev/null
+++ b/src/com/android/camera/ViewVideo.java
@@ -0,0 +1,210 @@
+/*
+ * 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.media.MediaPlayer;
+import android.app.Activity;
+import android.os.Bundle;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.Menu;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.MediaController;
+import android.view.Window;
+import android.widget.VideoView;
+import android.util.Config;
+
+class ViewVideo extends Activity
+{
+ static final String TAG = "ViewVideo";
+
+ private ImageManager.IImageList mAllVideos;
+ private PowerManager.WakeLock mWakeLock;
+ private ContentResolver mContentResolver;
+ private VideoView mVideoView;
+ private ImageManager.IImage mVideo;
+ private int mCurrentPosition = -1;
+ private MediaController mMediaController;
+
+ // if the activity gets paused the stash the current position here
+ int mPausedPlaybackPosition = 0;
+
+
+ public ViewVideo()
+ {
+ }
+
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ if (Config.LOGV)
+ Log.v(TAG, "onCreate");
+ //getWindow().setFormat(android.graphics.PixelFormat.TRANSLUCENT);
+
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+
+ mContentResolver = getContentResolver();
+
+ setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
+ setContentView(R.layout.viewvideo);
+
+ mMediaController = new MediaController(this);
+ mVideoView = (VideoView) findViewById(R.id.video);
+ mVideoView.setMediaController(mMediaController);
+ mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ public void onCompletion(MediaPlayer mp) {
+ // TODO what do we really want to do at the end of playback?
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle b) {
+ if (Config.LOGV)
+ Log.v(TAG, "onSaveInstanceState");
+ b.putInt("playback_position", mPausedPlaybackPosition);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle b) {
+ if (Config.LOGV)
+ Log.v(TAG, "onRestoreInstanceState");
+ mPausedPlaybackPosition = b.getInt("playback_position", 0);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (Config.LOGV)
+ Log.v(TAG, "onPause");
+ mAllVideos.deactivate();
+
+ mVideoView.pause();
+ mPausedPlaybackPosition = mVideoView.getCurrentPosition();
+ mVideoView.setVideoURI(null);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (Config.LOGV)
+ Log.v(TAG, "onStop");
+ }
+
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+ if (Config.LOGV)
+ Log.v(TAG, "onResume");
+
+ mAllVideos = ImageManager.instance().allImages(
+ ViewVideo.this,
+ mContentResolver,
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_DESCENDING);
+
+ // TODO smarter/faster here please
+ Uri uri = getIntent().getData();
+ if (mVideo == null) {
+ for (int i = 0; i < mAllVideos.getCount(); i++) {
+ ImageManager.IImage video = mAllVideos.getImageAt(i);
+ if (video.fullSizeImageUri().equals(uri)) {
+ mCurrentPosition = i;
+ mVideo = video;
+ break;
+ }
+ }
+ }
+
+ if (mCurrentPosition != -1) {
+ mMediaController.setPrevNextListeners(
+ new android.view.View.OnClickListener() {
+ public void onClick(View v) {
+ if (++mCurrentPosition == mAllVideos.getCount())
+ mCurrentPosition = 0;
+ ImageManager.IImage video = mAllVideos.getImageAt(mCurrentPosition);
+ mVideo = video;
+ mVideoView.setVideoURI(video.fullSizeImageUri());
+ mVideoView.start();
+ }
+ },
+ new android.view.View.OnClickListener() {
+ public void onClick(View v) {
+ if (--mCurrentPosition == -1)
+ mCurrentPosition = mAllVideos.getCount() - 1;
+ ImageManager.IImage video = mAllVideos.getImageAt(mCurrentPosition);
+ mVideo = video;
+ mVideoView.setVideoURI(video.fullSizeImageUri());
+ mVideoView.start();
+ }
+ });
+ }
+ if (Config.LOGV)
+ android.util.Log.v("camera", "seekTo " + mPausedPlaybackPosition);
+ mVideoView.setVideoURI(uri);
+ mVideoView.seekTo(mPausedPlaybackPosition);
+ mVideoView.start();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ super.onCreateOptionsMenu(menu);
+ MenuHelper.addVideoMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_VIEWPLAY_MENU,
+ ViewVideo.this, // activity
+ null, // handler
+ new SelectedImageGetter() {
+ public ImageManager.IImage getCurrentImage() {
+ return mVideo;
+ }
+ public Uri getCurrentImageUri() {
+ return mVideo.fullSizeImageUri();
+ }
+ },
+
+ // deletion case
+ new Runnable() {
+ public void run() {
+ mAllVideos.removeImage(mVideo);
+ finish();
+ }
+ },
+
+ // pre-work
+ null,
+
+ // post-work
+ null);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu)
+ {
+ return super.onPrepareOptionsMenu(menu);
+ }
+}
diff --git a/src/com/android/camera/Wallpaper.java b/src/com/android/camera/Wallpaper.java
new file mode 100644
index 0000000..ff41242
--- /dev/null
+++ b/src/com/android/camera/Wallpaper.java
@@ -0,0 +1,193 @@
+/*
+ * 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.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 Bitmap mBitmap;
+ private Handler mHandler;
+ private Context mContext;
+
+ public SetWallpaperThread(Bitmap bitmap, Handler handler, Context context) {
+ mBitmap = bitmap;
+ mHandler = handler;
+ mContext = context;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mContext.setWallpaper(mBitmap);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Failed to set wallpaper.", e);
+ } finally {
+ mHandler.sendEmptyMessage(FINISH);
+ }
+ }
+ }
+
+ 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) {
+ 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("output", Uri.parse("file:/" + mTempFilePath));
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if ((requestCode == PHOTO_PICKED || requestCode == CROP_DONE) && (resultCode == RESULT_OK)
+ && (data != null)) {
+ try {
+ InputStream s = new FileInputStream(mTempFilePath);
+ 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).start();
+ }
+ mDoLaunch = false;
+ } catch (FileNotFoundException ex) {
+
+ } catch (IOException ex) {
+
+ }
+ } else {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+}
diff --git a/src/com/android/camera/YouTubeUpload.java b/src/com/android/camera/YouTubeUpload.java
new file mode 100644
index 0000000..0963d09
--- /dev/null
+++ b/src/com/android/camera/YouTubeUpload.java
@@ -0,0 +1,145 @@
+/*
+ * 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.net.Uri;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.net.http.AndroidHttpClient;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+import android.util.Log;
+import android.util.Xml;
+
+
+public class YouTubeUpload extends Activity
+{
+ private static final String TAG = "YouTubeUpload";
+
+ private static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
+ private static final boolean mDevServer = false;
+
+ private ArrayList<String> mCategoriesShort = new ArrayList<String>();
+ private ArrayList<String> mCategoriesLong = new ArrayList<String>();
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ TextView tv = new TextView(this);
+ tv.setText("");
+ setContentView(tv);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final ProgressDialog pd = ProgressDialog.show(this, "please wait", "");
+
+ final ImageManager.IImageList all = ImageManager.instance().allImages(
+ YouTubeUpload.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_ASCENDING);
+
+ android.net.Uri uri = getIntent().getData();
+ if (uri == null) {
+ uri = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
+ }
+ if (uri != null) {
+ final ImageManager.VideoObject vid = (ImageManager.VideoObject) all.getImageForUri(uri);
+ if (vid != null) {
+ new Thread(new Runnable() {
+ public void run() {
+ getCategories();
+ runOnUiThread(new Runnable() {
+ public void run() {
+ pd.cancel();
+ MenuHelper.YouTubeUploadInfoDialog infoDialog = new MenuHelper.YouTubeUploadInfoDialog(
+ YouTubeUpload.this,
+ mCategoriesShort,
+ mCategoriesLong,
+ vid,
+ new Runnable() {
+ public void run() {
+ finish();
+ }
+ });
+ infoDialog.show();
+ }
+ });
+ }
+ }).start();
+ }
+ }
+ }
+
+ protected String getYouTubeBaseUrl() {
+ if (mDevServer) {
+ return "http://dev.gdata.youtube.com";
+ } else {
+ return "http://gdata.youtube.com";
+ }
+ }
+
+ public void getCategories() {
+ String uri = getYouTubeBaseUrl() + "/schemas/2007/categories.cat";
+ AndroidHttpClient mClient = AndroidHttpClient.newInstance("Android-Camera/0.1");
+
+ try {
+ org.apache.http.HttpResponse r = mClient.execute(new org.apache.http.client.methods.HttpGet(uri));
+ processReturnedData(r.getEntity().getContent());
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception getting categories... " + ex.toString());
+ }
+ }
+
+ public void processReturnedData(InputStream s) throws IOException, SAXException, XmlPullParserException {
+ try {
+ Xml.parse(s, Xml.findEncodingByName(null), new DefaultHandler() {
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ if (ATOM_NAMESPACE.equals(uri)) {
+ if ("category".equals(localName)) {
+ String catShortName = attributes.getValue("", "term");
+ String catLongName = attributes.getValue("", "label");
+ mCategoriesLong .add(catLongName);
+ mCategoriesShort.add(catShortName);
+ return;
+ }
+ }
+ }
+ });
+ } catch (SAXException e) {
+ e.printStackTrace();
+ }
+ }
+}