diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-02-19 10:57:35 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-02-19 10:57:35 -0800 |
commit | d8bccef54d490ce562e61fc907201da0c666314e (patch) | |
tree | f254350cce4028765c3b357809cbca4784b18d02 /src | |
parent | 5055ba62e22becc9c8d03898b80cda8ed68cfe80 (diff) | |
download | LegacyCamera-d8bccef54d490ce562e61fc907201da0c666314e.zip LegacyCamera-d8bccef54d490ce562e61fc907201da0c666314e.tar.gz LegacyCamera-d8bccef54d490ce562e61fc907201da0c666314e.tar.bz2 |
auto import from //branches/cupcake/...@132276
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/camera/Camera.java | 215 | ||||
-rw-r--r-- | src/com/android/camera/ImageGallery2.java | 5 | ||||
-rwxr-xr-x | src/com/android/camera/ImageManager.java | 82 | ||||
-rw-r--r-- | src/com/android/camera/OnScreenHint.java | 296 | ||||
-rw-r--r-- | src/com/android/camera/VideoCamera.java | 164 | ||||
-rw-r--r-- | src/com/android/camera/ViewImage.java | 111 |
6 files changed, 697 insertions, 176 deletions
diff --git a/src/com/android/camera/Camera.java b/src/com/android/camera/Camera.java index 079528b..9bf101c 100644 --- a/src/com/android/camera/Camera.java +++ b/src/com/android/camera/Camera.java @@ -16,7 +16,12 @@ package com.android.camera; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -68,12 +73,13 @@ import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; -import android.view.OrientationListener; +import android.view.OrientationEventListener; import android.view.SurfaceHolder; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.MenuItem.OnMenuItemClickListener; +import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; @@ -81,8 +87,6 @@ import android.view.animation.AnimationUtils; import android.widget.ImageView; import android.widget.Toast; -import com.android.camera.ImageManager.IImageList; - public class Camera extends Activity implements View.OnClickListener, ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback { @@ -117,8 +121,8 @@ public class Camera extends Activity implements View.OnClickListener, public static final int MENU_SAVE_CAMERA_VIDEO_DONE = 37; private Toast mToast; - private OrientationListener mOrientationListener; - private int mLastOrientation = OrientationListener.ORIENTATION_UNKNOWN; + private OrientationEventListener mOrientationListener; + private int mLastOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; private SharedPreferences mPreferences; private static final int IDLE = 1; @@ -132,7 +136,7 @@ public class Camera extends Activity implements View.OnClickListener, private android.hardware.Camera.Parameters mParameters; private VideoPreview mSurfaceView; private SurfaceHolder mSurfaceHolder = null; - private ImageView mBlackout = null; + private View mBlackout = null; private int mOriginalViewFinderWidth, mOriginalViewFinderHeight; private int mViewFinderWidth, mViewFinderHeight; @@ -165,6 +169,7 @@ public class Camera extends Activity implements View.OnClickListener, private Drawable[] mThumbnails; private boolean mShouldTransitionThumbnails; private Uri mLastPictureUri; + private Bitmap mLastPictureThumb; private LocationManager mLocationManager = null; private ShutterButton mShutterButton; @@ -182,6 +187,7 @@ public class Camera extends Activity implements View.OnClickListener, private boolean mKeepAndRestartPreview; + // mPostCaptureAlert is non-null only if isImageCaptureIntent() is true. private View mPostCaptureAlert; @@ -257,18 +263,16 @@ public class Camera extends Activity implements View.OnClickListener, 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(); + updateStorageHint(); } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED) || action.equals(Intent.ACTION_MEDIA_CHECKING)) { // SD card unavailable mPicturesRemaining = MenuHelper.NO_STORAGE_ERROR; - showStorageToast(mPicturesRemaining); + updateStorageHint(mPicturesRemaining); } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { Toast.makeText(Camera.this, getResources().getString(R.string.wait), 5000); } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) { - showStorageToast(); + updateStorageHint(); } } }; @@ -321,7 +325,6 @@ public class Camera extends Activity implements View.OnClickListener, Log.v(TAG, "********** Total shutter lag " + (now - mShutterPressTime) + " ms"); } if (mClickSound != null) { - mClickSound.seekTo(0); mClickSound.start(); } } @@ -450,17 +453,18 @@ public class Camera extends Activity implements View.OnClickListener, startTiming(); } long dateTaken = System.currentTimeMillis(); + String name = createName(dateTaken) + ".jpg"; mLastContentUri = ImageManager.instance().addImage( Camera.this, mContentResolver, - createName(dateTaken), + name, "", dateTaken, // location for the database goes here loc, 0, // the dsp will use the right orientation so don't "double set it" ImageManager.CAMERA_IMAGE_BUCKET_NAME, - null); + name); if (mLastContentUri == null) { // this means we got an error @@ -626,6 +630,10 @@ public class Camera extends Activity implements View.OnClickListener, mCameraDevice.setParameters(mParameters); mCameraDevice.takePicture(mShutterCallback, mRawPictureCallback, new JpegPictureCallback(loc)); + // Prepare the sound to play in shutter callback. + if (mClickSound != null) { + mClickSound.seekTo(0); + } mBlackout.setVisibility(View.VISIBLE); // Comment this out for now until we can decode the preview frame. This currently @@ -652,7 +660,7 @@ public class Camera extends Activity implements View.OnClickListener, // cached value which was calculated when the preview was restarted. if (DEBUG_TIME_OPERATIONS) mShutterPressTime = System.currentTimeMillis(); if (mPicturesRemaining < 1) { - showStorageToast(mPicturesRemaining); + updateStorageHint(mPicturesRemaining); return; } @@ -713,6 +721,7 @@ public class Camera extends Activity implements View.OnClickListener, if (mLastPictureButton.getVisibility() != View.VISIBLE) { mShouldShowLastPictureButton = true; } + mLastPictureThumb = lastPictureThumb; mLastPictureUri = uri; } @@ -759,12 +768,21 @@ public class Camera extends Activity implements View.OnClickListener, public void onCreate(Bundle icicle) { super.onCreate(icicle); + // To reduce startup time, we open camera device in another thread. + // We make sure the camera is opened at the end of onCreate. + Thread openCameraThread = new Thread(new Runnable() { + public void run() { + mCameraDevice = android.hardware.Camera.open(); + } + }); + openCameraThread.start(); + // To reduce startup time, we run some service creation code in another thread. // We make sure the services are loaded at the end of onCreate(). Thread loadServiceThread = new Thread(new Runnable() { public void run() { mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); - mOrientationListener = new OrientationListener(Camera.this) { + mOrientationListener = new OrientationEventListener(Camera.this) { public void onOrientationChanged(int orientation) { mLastOrientation = orientation; } @@ -776,9 +794,6 @@ public class Camera extends Activity implements View.OnClickListener, 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); @@ -792,28 +807,12 @@ public class Camera extends Activity implements View.OnClickListener, holder.addCallback(this); holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); - mBlackout = (ImageView) findViewById(R.id.blackout); - mBlackout.setBackgroundDrawable(new ColorDrawable(0xFF000000)); + mBlackout = findViewById(R.id.blackout); - mLastPictureButton = (ImageView) findViewById(R.id.last_picture_button); if (!isImageCaptureIntent()) { - ImageManager.IImageList images = ImageManager.instance().allImages( - this, - getContentResolver(), - ImageManager.DataLocation.ALL, - ImageManager.INCLUDE_IMAGES, - ImageManager.SORT_DESCENDING, - ImageManager.CAMERA_IMAGE_BUCKET_ID); - ImageManager.IImage lastPicture = - images.isEmpty() ? null : images.getImageAt(0); + mLastPictureButton = (ImageView) findViewById(R.id.last_picture_button); mLastPictureButton.setOnClickListener(this); - if (lastPicture == null) { - mLastPictureButton.setVisibility(View.GONE); - } else { - Bitmap miniThumb = lastPicture.miniThumbBitmap(); - setLastPictureThumb(miniThumb, lastPicture.fullSizeImageUri()); - } - images.deactivate(); + loadLastThumb(); } mShutterButton = (ShutterButton) findViewById(R.id.shutter_button); @@ -840,10 +839,17 @@ public class Camera extends Activity implements View.OnClickListener, mFocusBlinkAnimation.setRepeatCount(Animation.INFINITE); mFocusBlinkAnimation.setRepeatMode(Animation.REVERSE); - mPostCaptureAlert = findViewById(R.id.post_picture_panel); + // We load the post_picture_panel layout only if it is needed. + if (isImageCaptureIntent()) { + ViewGroup cameraView = (ViewGroup)findViewById(R.id.camera); + getLayoutInflater().inflate(R.layout.post_picture_panel, + cameraView); + mPostCaptureAlert = findViewById(R.id.post_picture_panel); + } // Make sure the services are loaded. try { + openCameraThread.join(); loadServiceThread.join(); } catch (InterruptedException ex) { } @@ -860,7 +866,7 @@ public class Camera extends Activity implements View.OnClickListener, if (!storageOK) { mHandler.post(new Runnable() { public void run() { - showStorageToast(mPicturesRemaining); + updateStorageHint(mPicturesRemaining); } }); } @@ -1005,12 +1011,37 @@ public class Camera extends Activity implements View.OnClickListener, } } - private void showStorageToast() { - MenuHelper.showStorageToast(this); + private void updateStorageHint() { + updateStorageHint(MenuHelper.calculatePicturesRemaining()); } - private void showStorageToast(int remainingPictures) { - MenuHelper.showStorageToast(this, remainingPictures); + private OnScreenHint mStorageHint; + + private void updateStorageHint(int remaining) { + String noStorageText = null; + + if (remaining == MenuHelper.NO_STORAGE_ERROR) { + String state = Environment.getExternalStorageState(); + if (state == Environment.MEDIA_CHECKING) { + noStorageText = getString(R.string.preparing_sd); + } else { + noStorageText = getString(R.string.no_storage); + } + } else if (remaining < 1) { + noStorageText = getString(R.string.not_enough_space); + } + + if (noStorageText != null) { + if (mStorageHint == null) { + mStorageHint = OnScreenHint.makeText(this, noStorageText); + } else { + mStorageHint.setText(noStorageText); + } + mStorageHint.show(); + } else if (mStorageHint != null) { + mStorageHint.cancel(); + mStorageHint = null; + } } @Override @@ -1048,20 +1079,78 @@ public class Camera extends Activity implements View.OnClickListener, } mBlackout.setVisibility(View.GONE); + } - if (mLastPictureUri != null) { - IImageList list = ImageManager.makeImageList(mLastPictureUri, this, - ImageManager.SORT_ASCENDING); - if (list.getImageForUri(mLastPictureUri) == null) { - mLastPictureUri = null; - mLastPictureButton.setVisibility(View.GONE); + private ImageManager.DataLocation dataLocation() { + return ImageManager.DataLocation.EXTERNAL; + } + + private static final int BUFSIZE = 4096; + + // Stores the thumbnail and URI of last-picture-taken to SD card, so we can + // load it the next time the Camera app starts. + private void storeLastThumb() { + if (mLastPictureUri != null && mLastPictureThumb != null) { + try { + FileOutputStream f = new FileOutputStream(ImageManager.getLastThumbPath()); + try { + BufferedOutputStream b = new BufferedOutputStream(f, BUFSIZE); + try { + DataOutputStream d = new DataOutputStream(b); + try { + d.writeUTF(mLastPictureUri.toString()); + mLastPictureThumb.compress(Bitmap.CompressFormat.PNG, 100, d); + } finally { + d.close(); + b = null; + f = null; + } + } finally { + if (b != null) { + b.close(); + f = null; + } + } + } finally { + if (f != null) { + f.close(); + } + } + } catch (IOException e) { } - list.deactivate(); } } - private ImageManager.DataLocation dataLocation() { - return ImageManager.DataLocation.EXTERNAL; + // Loads the thumbnail and URI of last-picture-taken from SD card. + private void loadLastThumb() { + try { + FileInputStream f = new FileInputStream(ImageManager.getLastThumbPath()); + try { + BufferedInputStream b = new BufferedInputStream(f, BUFSIZE); + try { + DataInputStream d = new DataInputStream(b); + try { + Uri lastUri = Uri.parse(d.readUTF()); + Bitmap lastThumb = BitmapFactory.decodeStream(d); + setLastPictureThumb(lastThumb, lastUri); + } finally { + d.close(); + b = null; + f = null; + } + } finally { + if (b != null) { + b.close(); + f = null; + } + } + } finally { + if (f != null) { + f.close(); + } + } + } catch (IOException e) { + } } @Override @@ -1096,6 +1185,11 @@ public class Camera extends Activity implements View.OnClickListener, mFocusToneGenerator = null; } + storeLastThumb(); + if (mStorageHint != null) { + mStorageHint.cancel(); + mStorageHint = null; + } super.onPause(); } @@ -1184,6 +1278,9 @@ public class Camera extends Activity implements View.OnClickListener, // If we get a dpad center event without any focused view, move the // focus to the shutter button and press it. if (event.getRepeatCount() == 0) { + // Start auto-focus immediately to reduce shutter lag. After the shutter button + // gets the focus, doFocus() will be called again but it is fine. + doFocus(true); if (mShutterButton.isInTouchMode()) { mShutterButton.requestFocusFromTouch(); } else { @@ -1299,7 +1396,7 @@ public class Camera extends Activity implements View.OnClickListener, Animation a = mShowLastPictureButtonAnimation; a.setDuration(500); mLastPictureButton.setAnimation(a); - } + } if (mShouldTransitionThumbnails) { mShouldTransitionThumbnails = false; @@ -1606,10 +1703,8 @@ public class Camera extends Activity implements View.OnClickListener, } private void showPostCaptureAlert() { - boolean isPick = isImageCaptureIntent(); - int pickVisible = isPick ? View.VISIBLE : View.GONE; - mPostCaptureAlert.setVisibility(pickVisible); - if (isPick) { + if (isImageCaptureIntent()) { + mPostCaptureAlert.setVisibility(View.VISIBLE); int[] pickIds = {R.id.attach, R.id.cancel}; for(int id : pickIds) { View view = mPostCaptureAlert.findViewById(id); @@ -1622,7 +1717,9 @@ public class Camera extends Activity implements View.OnClickListener, } private void hidePostCaptureAlert() { - mPostCaptureAlert.setVisibility(View.GONE); + if (isImageCaptureIntent()) { + mPostCaptureAlert.setVisibility(View.INVISIBLE); + } } @Override diff --git a/src/com/android/camera/ImageGallery2.java b/src/com/android/camera/ImageGallery2.java index 89afd9e..566bfcb 100644 --- a/src/com/android/camera/ImageGallery2.java +++ b/src/com/android/camera/ImageGallery2.java @@ -763,7 +763,7 @@ public class ImageGallery2 extends Activity { setVerticalScrollBarEnabled(true); initializeScrollbars(context.obtainStyledAttributes(android.R.styleable.View)); - mGestureDetector = new GestureDetector(context, new SimpleOnGestureListener() { + mGestureDetector = new GestureDetector(context, new SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { if (mScroller != null && !mScroller.isFinished()) { @@ -1464,7 +1464,8 @@ public class ImageGallery2 extends Activity { } return retVal; - }} + } + } Bitmap resizeBitmap(Bitmap b) { // assume they're both square for now diff --git a/src/com/android/camera/ImageManager.java b/src/com/android/camera/ImageManager.java index fb02f9e..4a83958 100755 --- a/src/com/android/camera/ImageManager.java +++ b/src/com/android/camera/ImageManager.java @@ -691,7 +691,17 @@ public class ImageManager { dbMagic = mMiniThumbMagic; byte [] data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic); if (data == null) { - dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow()); + byte[][] createdThumbData = new byte[1][]; + try { + dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), + getRow(), createdThumbData); + } catch (IOException ex) { + // Typically IOException because the sd card is full. + // But createdThumbData may have been filled in, so continue on. + } + data = createdThumbData[0]; + } + if (data == null) { data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic); } if (data == null) { @@ -724,7 +734,7 @@ public class ImageManager { mContainer.mCache.remove(mId); } - protected void saveMiniThumb(Bitmap source) { + protected void saveMiniThumb(Bitmap source) throws IOException { mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0); } @@ -1039,13 +1049,33 @@ public class ImageManager { } // returns id - public long checkThumbnail(BaseImage existingImage, Cursor c, int i) { + public long checkThumbnail(BaseImage existingImage, Cursor c, int i) throws IOException { + return checkThumbnail(existingImage, c, i, null); + } + + /** + * Checks to see if a mini thumbnail exists in the cache. If not, tries to create it and + * add it to the cache. + * @param existingImage + * @param c + * @param i + * @param createdThumbnailData if this parameter is non-null, and a new mini-thumbnail + * bitmap is created, the new bitmap's data will be stored in createdThumbnailData[0]. + * Note that if the sdcard is full, it's possible that + * createdThumbnailData[0] will be set even if the method throws an IOException. This is + * actually useful, because it allows the caller to use the created thumbnail even if + * the sdcard is full. + * @return + * @throws IOException + */ + public long checkThumbnail(BaseImage existingImage, Cursor c, int i, + byte[][] createdThumbnailData) throws IOException { long magic, fileMagic = 0, id; try { mLock.lock(); if (existingImage == null) { // if we don't have an Image object then get the id and magic from - // the cursor. Synchonize on the cursor object. + // the cursor. Synchronize on the cursor object. synchronized (c) { if (!c.moveToPosition(i)) { return -1; @@ -1090,7 +1120,6 @@ public class ImageManager { // 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)) { @@ -1129,8 +1158,11 @@ public class ImageManager { magic = mRandom.nextLong(); } while (magic == 0); if (bitmap != null) { - saveMiniThumbToFile(bitmap, id, magic); - bitmap.recycle(); + byte [] data = miniThumbData(bitmap); + if (createdThumbnailData != null) { + createdThumbnailData[0] = data; + } + saveMiniThumbToFile(data, id, magic); } synchronized (c) { @@ -1483,7 +1515,12 @@ public class ImageManager { mCursorDeactivated = false; } - protected void saveMiniThumbToFile(Bitmap source, long id, long magic) { + protected void saveMiniThumbToFile(Bitmap bitmap, long id, long magic) throws IOException { + byte[] data = miniThumbData(bitmap); + saveMiniThumbToFile(data, id, magic); + } + + protected void saveMiniThumbToFile(byte[] data, long id, long magic) throws IOException { RandomAccessFile r = miniThumbDataFile(); if (r == null) return; @@ -1493,7 +1530,6 @@ public class ImageManager { synchronized (r) { try { long t1 = System.currentTimeMillis(); - byte [] data = miniThumbData(source); long t2 = System.currentTimeMillis(); if (data != null) { if (data.length > sBytesPerMiniThumb) { @@ -1518,9 +1554,8 @@ public class ImageManager { if (VERBOSE) Log.v(TAG, "saveMiniThumbToFile took " + (t3-t0) + "; " + (t1-t0) + " " + (t2-t1) + " " + (t3-t2)); } } catch (IOException ex) { - if (VERBOSE) { - Log.e(TAG, "couldn't save mini thumbnail data for " + id + "; " + ex.toString()); - } + Log.e(TAG, "couldn't save mini thumbnail data for " + id + "; " + ex.toString()); + throw ex; } } } @@ -1929,7 +1964,11 @@ public class ImageManager { long t4 = System.currentTimeMillis(); checkCanceled(); if (VERBOSE) Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>> rotating by " + orientation); - saveMiniThumb(rotate(thumbnail, orientation)); + try { + saveMiniThumb(rotate(thumbnail, orientation)); + } catch (IOException e) { + // Ignore if unable to save thumb. + } long t5 = System.currentTimeMillis(); checkCanceled(); @@ -2007,7 +2046,11 @@ public class ImageManager { // setting this to zero will force the call to checkCursor to generate fresh thumbs mMiniThumbMagic = 0; - mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow()); + try { + mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow()); + } catch (IOException e) { + // Ignore inability to store mini thumbnail. + } return true; } @@ -3604,8 +3647,10 @@ public class ImageManager { return sInstance; } - - static public byte [] miniThumbData(Bitmap source) { + /** + * Creates a byte[] for a given bitmap of the desired size. Recycles the input bitmap. + */ + static public byte[] miniThumbData(Bitmap source) { if (source == null) return null; @@ -4130,4 +4175,9 @@ public class ImageManager { } return bitmap; } + + public static String getLastThumbPath() { + return Environment.getExternalStorageDirectory().toString() + + "/DCIM/.thumbnails/camera_last_thumb"; + } } diff --git a/src/com/android/camera/OnScreenHint.java b/src/com/android/camera/OnScreenHint.java new file mode 100644 index 0000000..96190a0 --- /dev/null +++ b/src/com/android/camera/OnScreenHint.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.app.INotificationManager; +import android.app.ITransientNotification; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.os.RemoteException; +import android.os.Handler; +import android.os.ServiceManager; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +/** + * A on-screen hint is a view containing a little message for the user and will + * be shown on the screen continuously. This class helps you create and show + * those. + * + * <p> + * When the view is shown to the user, appears as a floating view over the + * application. + * <p> + * The easiest way to use this class is to call one of the static methods that + * constructs everything you need and returns a new OnScreenHint object. + */ +public class OnScreenHint { + static final String TAG = "OnScreenHint"; + static final boolean localLOGV = false; + + final Context mContext; + int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + int mX, mY; + float mHorizontalMargin; + float mVerticalMargin; + View mView; + View mNextView; + + private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); + private WindowManager mWM; + private final Handler mHandler = new Handler(); + + /** + * Construct an empty OnScreenHint object. You must call {@link #setView} before you + * can call {@link #show}. + * + * @param context The context to use. Usually your {@link android.app.Application} + * or {@link android.app.Activity} object. + */ + public OnScreenHint(Context context) { + mContext = context; + mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mY = context.getResources().getDimensionPixelSize(R.dimen.hint_y_offset); + + mParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + mParams.width = WindowManager.LayoutParams.WRAP_CONTENT; + mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + mParams.format = PixelFormat.TRANSLUCENT; + mParams.windowAnimations = R.style.Animation_OnScreenHint; + mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + mParams.setTitle("OnScreenHint"); + } + + /** + * Show the view on the screen. + */ + public void show() { + if (mNextView == null) { + throw new RuntimeException("setView must have been called"); + } + if (localLOGV) Log.v(TAG, "SHOW: " + this); + mHandler.post(mShow); + } + + /** + * Close the view if it's showing. + */ + public void cancel() { + if (localLOGV) Log.v(TAG, "HIDE: " + this); + mHandler.post(mHide); + } + + /** + * Set the view to show. + * @see #getView + */ + public void setView(View view) { + mNextView = view; + } + + /** + * Return the view. + * @see #setView + */ + public View getView() { + return mNextView; + } + + /** + * Set the margins of the view. + * + * @param horizontalMargin The horizontal margin, in percentage of the + * container width, between the container's edges and the + * notification + * @param verticalMargin The vertical margin, in percentage of the + * container height, between the container's edges and the + * notification + */ + public void setMargin(float horizontalMargin, float verticalMargin) { + mHorizontalMargin = horizontalMargin; + mVerticalMargin = verticalMargin; + } + + /** + * Return the horizontal margin. + */ + public float getHorizontalMargin() { + return mHorizontalMargin; + } + + /** + * Return the vertical margin. + */ + public float getVerticalMargin() { + return mVerticalMargin; + } + + /** + * Set the location at which the notification should appear on the screen. + * @see android.view.Gravity + * @see #getGravity + */ + public void setGravity(int gravity, int xOffset, int yOffset) { + mGravity = gravity; + mX = xOffset; + mY = yOffset; + } + + /** + * Get the location at which the notification should appear on the screen. + * @see android.view.Gravity + * @see #getGravity + */ + public int getGravity() { + return mGravity; + } + + /** + * Return the X offset in pixels to apply to the gravity's location. + */ + public int getXOffset() { + return mX; + } + + /** + * Return the Y offset in pixels to apply to the gravity's location. + */ + public int getYOffset() { + return mY; + } + + /** + * Make a standard hint that just contains a text view. + * + * @param context The context to use. Usually your {@link android.app.Application} + * or {@link android.app.Activity} object. + * @param text The text to show. Can be formatted text. + * + */ + public static OnScreenHint makeText(Context context, CharSequence text) { + OnScreenHint result = new OnScreenHint(context); + + LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View v = inflate.inflate(R.layout.on_screen_hint, null); + TextView tv = (TextView)v.findViewById(R.id.message); + tv.setText(text); + + result.mNextView = v; + + return result; + } + + /** + * Make a standard hint that just contains a text view with the text from a resource. + * + * @param context The context to use. Usually your {@link android.app.Application} + * or {@link android.app.Activity} object. + * @param resId The resource id of the string resource to use. Can be formatted text. + * + * @throws Resources.NotFoundException if the resource can't be found. + */ + public static OnScreenHint makeText(Context context, int resId) + throws Resources.NotFoundException { + return makeText(context, context.getResources().getText(resId)); + } + + /** + * Update the text in a OnScreenHint that was previously created using one of the makeText() methods. + * @param resId The new text for the OnScreenHint. + */ + public void setText(int resId) { + setText(mContext.getText(resId)); + } + + /** + * Update the text in a OnScreenHint that was previously created using one of the makeText() methods. + * @param s The new text for the OnScreenHint. + */ + public void setText(CharSequence s) { + if (mNextView == null) { + throw new RuntimeException("This OnScreenHint was not created with OnScreenHint.makeText()"); + } + TextView tv = (TextView) mNextView.findViewById(R.id.message); + if (tv == null) { + throw new RuntimeException("This OnScreenHint was not created with OnScreenHint.makeText()"); + } + tv.setText(s); + } + + private synchronized void handleShow() { + if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + + " mNextView=" + mNextView); + if (mView != mNextView) { + // remove the old view if necessary + handleHide(); + mView = mNextView; + final int gravity = mGravity; + mParams.gravity = gravity; + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { + mParams.horizontalWeight = 1.0f; + } + if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { + mParams.verticalWeight = 1.0f; + } + mParams.x = mX; + mParams.y = mY; + mParams.verticalMargin = mVerticalMargin; + mParams.horizontalMargin = mHorizontalMargin; + if (mView.getParent() != null) { + if (localLOGV) Log.v( + TAG, "REMOVE! " + mView + " in " + this); + mWM.removeView(mView); + } + if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this); + mWM.addView(mView, mParams); + } + } + + private synchronized void handleHide() { + if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); + if (mView != null) { + // note: checking parent() just to make sure the view has + // been added... i have seen cases where we get here when + // the view isn't yet added, so let's try not to crash. + if (mView.getParent() != null) { + if (localLOGV) Log.v( + TAG, "REMOVE! " + mView + " in " + this); + mWM.removeView(mView); + } + mView = null; + } + } + + private Runnable mShow = new Runnable() { + public void run() { + handleShow(); + } + }; + + private Runnable mHide = new Runnable() { + public void run() { + handleHide(); + } + }; +} + diff --git a/src/com/android/camera/VideoCamera.java b/src/com/android/camera/VideoCamera.java index e3b7ebe..05e33a6 100644 --- a/src/com/android/camera/VideoCamera.java +++ b/src/com/android/camera/VideoCamera.java @@ -17,6 +17,7 @@ package com.android.camera; import java.io.File; +import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -108,6 +109,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, // The video file that the hardware camera is about to record into // (or is recording into.) private String mCameraVideoFilename; + private FileDescriptor mCameraVideoFileDescriptor; // The video file that has already been recorded, and that is being // examined by the user. @@ -197,16 +199,16 @@ public class VideoCamera extends Activity implements View.OnClickListener, // SD card available // TODO put up a "please wait" message // TODO also listen for the media scanner finished message - showStorageToast(); + updateStorageHint(); mHasSdCard = true; } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) { // SD card unavailable - showStorageToast(); + updateStorageHint(); mHasSdCard = false; } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { Toast.makeText(VideoCamera.this, getResources().getString(R.string.wait), 5000); } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) { - showStorageToast(); + updateStorageHint(); } } }; @@ -273,7 +275,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, if (!storageOK) { mHandler.post(new Runnable() { public void run() { - showStorageToast(); + updateStorageHint(); } }); } @@ -285,6 +287,10 @@ public class VideoCamera extends Activity implements View.OnClickListener, public void onClick(View v) { switch (v.getId()) { + case R.id.gallery: + MenuHelper.gotoCameraVideoGallery(this); + break; + case R.id.attach: doReturnToCaller(true); break; @@ -362,15 +368,30 @@ public class VideoCamera extends Activity implements View.OnClickListener, hideVideoFrameAndStartPreview(); } - private void showStorageToast() { - long remaining = getAvailableStorage(); + private OnScreenHint mStorageHint; + private void updateStorageHint() { + long remaining = getAvailableStorage(); + String errorMessage = null; if (remaining == NO_STORAGE_ERROR) { - Toast.makeText(this, getString(R.string.no_storage), Toast.LENGTH_LONG).show(); + errorMessage = getString(R.string.no_storage); } else if (remaining < LOW_STORAGE_THRESHOLD) { - new AlertDialog.Builder(this).setTitle(R.string.spaceIsLow_title) - .setMessage(R.string.spaceIsLow_content) - .show(); + errorMessage = getString(R.string.spaceIsLow_content); + if (mStorageHint != null) { + mStorageHint.cancel(); + mStorageHint = null; + } + } + if (errorMessage != null) { + if (mStorageHint == null) { + mStorageHint = OnScreenHint.makeText(this, errorMessage); + } else { + mStorageHint.setText(errorMessage); + } + mStorageHint.show(); + } else if (mStorageHint != null) { + mStorageHint.cancel(); + mStorageHint = null; } } @@ -431,6 +452,11 @@ public class VideoCamera extends Activity implements View.OnClickListener, } mBlackout.setVisibility(View.VISIBLE); setScreenTimeoutSystemDefault(); + + if (mStorageHint != null) { + mStorageHint.cancel(); + mStorageHint = null; + } } @Override @@ -576,53 +602,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, int resultCode; if (success) { resultCode = RESULT_OK; - Uri saveUri = null; - - Bundle myExtras = getIntent().getExtras(); - if (myExtras != null) { - saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); - } - - if (saveUri != null) { - // TODO: Record the video directly into the content provider stream when - // bug 1582062 is fixed. Until then we copy the video data from the - // original location to the requested location and then delete the original. - OutputStream outputStream = null; - InputStream inputStream = null; - - try { - inputStream = mContentResolver.openInputStream(mCurrentVideoUri); - outputStream = mContentResolver.openOutputStream(saveUri); - byte[] buffer = new byte[64*1024]; - while(true) { - int bytesRead = inputStream.read(buffer); - if (bytesRead < 0) { - break; - } - outputStream.write(buffer, 0, bytesRead); - } - } catch (IOException ex) { - Log.e(TAG, "Could not copy video file to Uri", ex); - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException ex) { - Log.e(TAG, "Could not close video file", ex); - } - } - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException ex) { - Log.e(TAG, "Could not close output uri", ex); - } - } - deleteCurrentVideo(); - } - } else { - resultIntent.setData(mCurrentVideoUri); - } + resultIntent.setData(mCurrentVideoUri); } else { resultCode = RESULT_CANCELED; } @@ -651,8 +631,36 @@ public class VideoCamera extends Activity implements View.OnClickListener, } } + private void cleanupEmptyFile() { + if (mCameraVideoFilename != null) { + File f = new File(mCameraVideoFilename); + if (f.length() == 0 && f.delete()) { + Log.v(TAG, "Empty video file deleted: " + mCameraVideoFilename); + mCameraVideoFilename = null; + } + } + } + private void initializeVideo() { Log.v(TAG, "initializeVideo"); + boolean isCaptureIntent = isVideoCaptureIntent(); + Intent intent = getIntent(); + Bundle myExtras = intent.getExtras(); + + if (isCaptureIntent && myExtras != null) { + Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + if (saveUri != null) { + try { + mCameraVideoFileDescriptor = mContentResolver. + openFileDescriptor(saveUri, "rw").getFileDescriptor(); + mCurrentVideoUri = saveUri; + } + catch (java.io.FileNotFoundException ex) { + // invalid uri + Log.e(TAG, ex.toString()); + } + } + } releaseMediaRecorder(); if (mSurfaceHolder == null) { @@ -670,17 +678,22 @@ public class VideoCamera extends Activity implements View.OnClickListener, } mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); - createVideoPath(); - mMediaRecorder.setOutputFile(mCameraVideoFilename); + + // We try Uri in intent first. If it doesn't work, use our own instead. + if (mCameraVideoFileDescriptor != null) { + mMediaRecorder.setOutputFile(mCameraVideoFileDescriptor); + } else { + createVideoPath(); + mMediaRecorder.setOutputFile(mCameraVideoFilename); + } + boolean videoQualityHigh = getBooleanPreference(CameraSettings.KEY_VIDEO_QUALITY, CameraSettings.DEFAULT_VIDEO_QUALITY_VALUE); - { - Intent intent = getIntent(); - if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { - int extraVideoQuality = intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); - videoQualityHigh = (extraVideoQuality > 0); - } + + if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { + int extraVideoQuality = intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); + videoQualityHigh = (extraVideoQuality > 0); } // Use the same frame rate for both, since internally @@ -712,6 +725,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, private void releaseMediaRecorder() { Log.v(TAG, "Releasing media recorder."); if (mMediaRecorder != null) { + cleanupEmptyFile(); mMediaRecorder.reset(); mMediaRecorder.release(); mMediaRecorder = null; @@ -773,10 +787,12 @@ public class VideoCamera extends Activity implements View.OnClickListener, } private void registerVideo() { - Uri videoTable = Uri.parse("content://media/external/video/media"); - mCurrentVideoUri = mContentResolver.insert(videoTable, - mCurrentVideoValues); - Log.v(TAG, "Current video URI: " + mCurrentVideoUri); + if (mCameraVideoFileDescriptor == null) { + Uri videoTable = Uri.parse("content://media/external/video/media"); + mCurrentVideoUri = mContentResolver.insert(videoTable, + mCurrentVideoValues); + Log.v(TAG, "Current video URI: " + mCurrentVideoUri); + } mCurrentVideoValues = null; } @@ -837,10 +853,8 @@ public class VideoCamera extends Activity implements View.OnClickListener, Log.v(TAG, "startVideoRecording"); if (!mMediaRecorderRecording) { - if (!mHasSdCard) { - Toast.makeText(this, getString( - R.string.no_storage), Toast.LENGTH_LONG).show(); - Log.v(TAG, "No SD card, ignore start recording"); + if (mStorageHint != null) { + Log.v(TAG, "Storage issue, ignore the start request"); return; } @@ -882,7 +896,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, private void showPostRecordingAlert() { int[] pickIds = {R.id.attach, R.id.cancel}; - int[] normalIds = {R.id.share, R.id.discard}; + int[] normalIds = {R.id.gallery, R.id.share, R.id.discard}; int[] alwaysOnIds = {R.id.play}; int[] hideIds = pickIds; int[] connectIds = normalIds; @@ -923,7 +937,6 @@ public class VideoCamera extends Activity implements View.OnClickListener, mMediaRecorder.stop(); mCurrentVideoFilename = mCameraVideoFilename; Log.v(TAG, "Setting current video filename: " + mCurrentVideoFilename); - mCameraVideoFilename = null; mNeedToRegisterRecording = true; mMediaRecorderRecording = false; } @@ -937,6 +950,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, mNeedToRegisterRecording = false; } mCameraVideoFilename = null; + mCameraVideoFileDescriptor = null; } private void setScreenTimeoutSystemDefault() { diff --git a/src/com/android/camera/ViewImage.java b/src/com/android/camera/ViewImage.java index 56150ac..57d7051 100644 --- a/src/com/android/camera/ViewImage.java +++ b/src/com/android/camera/ViewImage.java @@ -39,6 +39,7 @@ 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.view.WindowManager; import android.view.animation.AlphaAnimation; @@ -237,7 +238,36 @@ public class ViewImage extends Activity implements View.OnClickListener // The event time of the previous touch up. private long mPreviousUpTime; // The duration in milliseconds we will wait to see if it is a double tap. - private static final int DOUBLE_TAP_TIMEOUT = 200; + private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); + + // Returns current scale step (numbered from 0 to 10). + private int getCurrentStep() { + float s = getScale(); + float b = sScaleRate; + int step = (int)Math.round(Math.log(s) / Math.log(b)); + return Math.max(0, Math.min(10, step)); + } + + // Returns the max scale step this image can use. + private int getMaxStep() { + float s = maxZoom(); + float b = sScaleRate; + int step = (int)Math.ceil(Math.log(s) / Math.log(b)); + return Math.max(0, Math.min(10, step)); + } + + // The setup we use here is to have 12 steps (only 0 to 10 are used), + // each separated by angle PI/6. We allow clockwise rotation (zoom-in) + // from the beginning position only. So we set counter clockwise bound + // to 0 and set clockwise bound to (2 - N/6) * PI. (clockwise angle + // is negative, and we need to mod 2*PI for the API to work.) + private void setZoomRingBounds() { + int max_step = getMaxStep(); + float max_angle = (2 - max_step / 6F) * (float)Math.PI; + mZoomRingController.setResetThumbAutomatically(false); + mZoomRingController.setThumbClockwiseBound(max_angle); + mZoomRingController.setThumbCounterclockwiseBound(0); + } // The zoom ring is set to visible by a double tap. private ZoomRingController mZoomRingController; @@ -246,38 +276,69 @@ public class ViewImage extends Activity implements View.OnClickListener public void onCenter(int x, int y) { } + public void onBeginPan() { + } + public boolean onPan(int deltaX, int deltaY) { postTranslate(-deltaX, -deltaY, sUseBounce); ImageViewTouch.this.center(true, true, false); return true; } + public void onEndPan() { + } + + // The clockwise angle is negative, so we need to mod 2*PI + private float stepToAngle(int step) { + float angle = step * (float)Math.PI / 6; + angle = (float)Math.PI * 2 - angle; + return angle; + } + + private int angleToStep(double angle) { + angle = Math.PI * 2 - angle; + int step = (int)Math.round(angle / (Math.PI / 6)); + return step; + } + public void onVisibilityChanged(boolean visible) { + if (visible) { + int step = getCurrentStep(); + float angle = stepToAngle(step); + mZoomRingController.setThumbAngle(angle); + } } - public void onBeginDrag(float startAngle) { + public void onBeginDrag() { + setZoomRingBounds(); } - public void onEndDrag(float endAngle) { + public void onEndDrag() { } public boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, float curAngle) { + setZoomRingBounds(); + int deltaStep = angleToStep(curAngle) - getCurrentStep(); + if ((deltaZoomLevel > 0) && (deltaStep < 0)) return false; + if ((deltaZoomLevel < 0) && (deltaStep > 0)) return false; + if ((deltaZoomLevel == 0) || (deltaStep == 0)) return false; + float oldScale = getScale(); // First move centerX/centerY to the center of the view. int deltaX = getWidth() / 2 - centerX; int deltaY = getHeight() / 2 - centerY; panBy(deltaX, deltaY); - + // Do zoom in/out. - while (deltaZoomLevel > 0) { + while (deltaStep > 0) { zoomIn(); - deltaZoomLevel--; + deltaStep--; } - while (deltaZoomLevel < 0) { + while (deltaStep < 0) { zoomOut(); - deltaZoomLevel++; + deltaStep++; } // Reverse the first centering. @@ -335,11 +396,16 @@ public class ViewImage extends Activity implements View.OnClickListener switch (m.getAction()) { case MotionEvent.ACTION_DOWN: - viewImage.setMode(MODE_NORMAL); - viewImage.showOnScreenControls(); - mLastXTouchPos = x; - mLastYTouchPos = y; - mTouchState = TOUCH_STATE_REST; + long downTime = m.getEventTime(); + if ((downTime - mPreviousUpTime) < DOUBLE_TAP_TIMEOUT) { + mZoomRingController.setVisible(true); + } else { + viewImage.setMode(MODE_NORMAL); + viewImage.showOnScreenControls(); + mLastXTouchPos = x; + mLastYTouchPos = y; + mTouchState = TOUCH_STATE_REST; + } break; case MotionEvent.ACTION_MOVE: if (x < TOUCH_AREA_WIDTH) { @@ -413,15 +479,7 @@ public class ViewImage extends Activity implements View.OnClickListener viewImage.mPrevImageView.setPressed(false); viewImage.mNextImageView.setPressed(false); mTouchState = TOUCH_STATE_REST; - - long eventTime = m.getEventTime(); - if (eventTime - mPreviousUpTime < DOUBLE_TAP_TIMEOUT) { - mZoomRingController.setVisible(true); - mPreviousUpTime = 0; - } else { - mPreviousUpTime = eventTime; - } - + mPreviousUpTime = m.getEventTime(); break; case MotionEvent.ACTION_CANCEL: viewImage.mPrevImageView.setPressed(false); @@ -530,6 +588,11 @@ public class ViewImage extends Activity implements View.OnClickListener return scrollHandler().getScrollX(); } + @Override + protected void onDetachedFromWindow() { + mZoomRingController.setVisible(false); + } + } static class ScrollHandler extends LinearLayout { @@ -1544,12 +1607,12 @@ public class ViewImage extends Activity implements View.OnClickListener mAllImages.deactivate(); - for (ImageViewTouchBase iv: mImageViews) { + for (ImageViewTouch iv: mImageViews) { iv.recycleBitmaps(); iv.setImageBitmap(null, true); } - for (ImageViewTouchBase iv: mSlideShowImageViews) { + for (ImageViewTouch iv: mSlideShowImageViews) { iv.recycleBitmaps(); iv.setImageBitmap(null, true); } |