From ce5480e099fda944b9e96e4b750300944c3f4a4f Mon Sep 17 00:00:00 2001 From: Angus Kong Date: Tue, 29 Jan 2013 17:43:48 -0800 Subject: Make background media saving a service bug:8091328 Change-Id: If35c1832238e921b6582d8642fdbaa4378ea0c48 --- src/com/android/camera/MediaSaveService.java | 155 +++++++++++++++++++++++++++ src/com/android/camera/MediaSaver.java | 149 ------------------------- src/com/android/camera/PhotoModule.java | 97 +++++++++++------ 3 files changed, 219 insertions(+), 182 deletions(-) create mode 100644 src/com/android/camera/MediaSaveService.java delete mode 100644 src/com/android/camera/MediaSaver.java (limited to 'src/com/android/camera') diff --git a/src/com/android/camera/MediaSaveService.java b/src/com/android/camera/MediaSaveService.java new file mode 100644 index 000000000..f8719756c --- /dev/null +++ b/src/com/android/camera/MediaSaveService.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.app.Service; +import android.content.ContentResolver; +import android.content.Intent; +import android.location.Location; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; + +import java.util.ArrayList; + +public class MediaSaveService extends Service { + private static final int SAVE_TASK_LIMIT = 3; + private static final String TAG = MediaSaveService.class.getSimpleName(); + + private final IBinder mBinder = new LocalBinder(); + private int mTaskNumber; + private Listener mListener; + + interface Listener { + public void onQueueAvailable(); + public void onQueueFull(); + } + + interface OnMediaSavedListener { + public void onMediaSaved(Uri uri); + } + + class LocalBinder extends Binder { + public MediaSaveService getService() { + return MediaSaveService.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flag, int startId) { + return START_STICKY; + } + + @Override + public void onDestroy() { + } + + @Override + public void onCreate() { + mTaskNumber = 0; + } + + public boolean isQueueFull() { + return (mTaskNumber >= SAVE_TASK_LIMIT); + } + + // Runs in main thread + public void addImage(final byte[] data, String title, long date, Location loc, + int width, int height, int orientation, + OnMediaSavedListener l, ContentResolver resolver) { + if (isQueueFull()) { + Log.e(TAG, "Cannot add image when the queue is full"); + return; + } + SaveTask t = new SaveTask(data, title, date, (loc == null) ? null : new Location(loc), + width, height, orientation, resolver, l); + + mTaskNumber++; + if (isQueueFull()) { + onQueueFull(); + } + t.execute(); + } + + public void setListener(Listener l) { + mListener = l; + if (l == null) return; + if (isQueueFull()) { + l.onQueueFull(); + } else { + l.onQueueAvailable(); + } + } + + private void onQueueFull() { + if (mListener != null) mListener.onQueueFull(); + } + + private void onQueueAvailable() { + if (mListener != null) mListener.onQueueAvailable(); + } + + private class SaveTask extends AsyncTask { + private byte[] data; + private String title; + private long date; + private Location loc; + private int width, height; + private int orientation; + private ContentResolver resolver; + private OnMediaSavedListener listener; + + public SaveTask(byte[] data, String title, long date, Location loc, + int width, int height, int orientation, ContentResolver resolver, + OnMediaSavedListener listener) { + this.data = data; + this.title = title; + this.date = date; + this.loc = loc; + this.width = width; + this.height = height; + this.orientation = orientation; + this.resolver = resolver; + this.listener = listener; + } + + @Override + protected void onPreExecute() { + // do nothing. + } + + @Override + protected Uri doInBackground(Void... v) { + return Storage.addImage( + resolver, title, date, loc, orientation, data, width, height); + } + + @Override + protected void onPostExecute(Uri uri) { + listener.onMediaSaved(uri); + mTaskNumber--; + if (mTaskNumber == SAVE_TASK_LIMIT - 1) onQueueAvailable(); + } + } +} diff --git a/src/com/android/camera/MediaSaver.java b/src/com/android/camera/MediaSaver.java deleted file mode 100644 index a3d582e1c..000000000 --- a/src/com/android/camera/MediaSaver.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.camera; - -import android.content.ContentResolver; -import android.location.Location; -import android.net.Uri; -import android.util.Log; - -import java.util.ArrayList; - -// We use a queue to store the SaveRequests that have not been completed -// yet. The main thread puts the request into the queue. The saver thread -// gets it from the queue, does the work, and removes it from the queue. -// -// The main thread needs to wait for the saver thread to finish all the work -// in the queue, when the activity's onPause() is called, we need to finish -// all the work, so other programs (like Gallery) can see all the images. -// -// If the queue becomes too long, adding a new request will block the main -// thread until the queue length drops below the threshold (QUEUE_LIMIT). -// If we don't do this, we may face several problems: (1) We may OOM -// because we are holding all the jpeg data in memory. (2) We may ANR -// when we need to wait for saver thread finishing all the work (in -// onPause() or gotoGallery()) because the time to finishing a long queue -// of work may be too long. -class MediaSaver extends Thread { - private static final int SAVE_QUEUE_LIMIT = 3; - private static final String TAG = "MediaSaver"; - - private ArrayList mQueue; - private boolean mStop; - private ContentResolver mContentResolver; - - public interface OnMediaSavedListener { - public void onMediaSaved(Uri uri); - } - - public MediaSaver(ContentResolver resolver) { - mContentResolver = resolver; - mQueue = new ArrayList(); - start(); - } - - // Runs in main thread - public synchronized boolean queueFull() { - return (mQueue.size() >= SAVE_QUEUE_LIMIT); - } - - // Runs in main thread - public void addImage(final byte[] data, String title, long date, Location loc, - int width, int height, int orientation, OnMediaSavedListener l) { - SaveRequest r = new SaveRequest(); - r.data = data; - r.date = date; - r.title = title; - r.loc = (loc == null) ? null : new Location(loc); // make a copy - r.width = width; - r.height = height; - r.orientation = orientation; - r.listener = l; - synchronized (this) { - while (mQueue.size() >= SAVE_QUEUE_LIMIT) { - try { - wait(); - } catch (InterruptedException ex) { - // ignore. - } - } - mQueue.add(r); - notifyAll(); // Tell saver thread there is new work to do. - } - } - - // Runs in saver thread - @Override - public void run() { - while (true) { - SaveRequest r; - synchronized (this) { - if (mQueue.isEmpty()) { - notifyAll(); // notify main thread in waitDone - - // Note that we can only stop after we saved all images - // in the queue. - if (mStop) break; - - try { - wait(); - } catch (InterruptedException ex) { - // ignore. - } - continue; - } - if (mStop) break; - r = mQueue.remove(0); - notifyAll(); // the main thread may wait in addImage - } - Uri uri = storeImage(r.data, r.title, r.date, r.loc, r.width, r.height, - r.orientation); - r.listener.onMediaSaved(uri); - } - if (!mQueue.isEmpty()) { - Log.e(TAG, "Media saver thread stopped with " + mQueue.size() + " images unsaved"); - mQueue.clear(); - } - } - - // Runs in main thread - public void finish() { - synchronized (this) { - mStop = true; - notifyAll(); - } - } - - // Runs in saver thread - private Uri storeImage(final byte[] data, String title, long date, - Location loc, int width, int height, int orientation) { - Uri uri = Storage.addImage(mContentResolver, title, date, loc, - orientation, data, width, height); - return uri; - } - - // Each SaveRequest remembers the data needed to save an image. - private static class SaveRequest { - byte[] data; - String title; - long date; - Location loc; - int width, height; - int orientation; - OnMediaSavedListener listener; - } -} diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java index a283e5949..14477b7c1 100644 --- a/src/com/android/camera/PhotoModule.java +++ b/src/com/android/camera/PhotoModule.java @@ -19,10 +19,13 @@ package com.android.camera; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; +import android.content.ComponentName; import android.content.ContentProviderClient; import android.content.ContentResolver; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.ServiceConnection; import android.content.SharedPreferences.Editor; import android.content.res.Configuration; import android.graphics.Bitmap; @@ -39,6 +42,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.ConditionVariable; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; @@ -95,7 +99,8 @@ public class PhotoModule ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback, PieRenderer.PieListener, - CountDownView.OnCountDownFinishedListener { + CountDownView.OnCountDownFinishedListener, + MediaSaveService.Listener { private static final String TAG = "CAM_PhotoModule"; @@ -114,7 +119,6 @@ public class PhotoModule private static final int START_PREVIEW_DONE = 10; private static final int OPEN_CAMERA_FAIL = 11; private static final int CAMERA_DISABLED = 12; - private static final int UPDATE_SECURE_ALBUM_ITEM = 13; // The subset of parameters we need to update in setCameraParameters(). private static final int UPDATE_PARAM_INITIALIZE = 1; @@ -200,11 +204,21 @@ public class PhotoModule // A view group that contains all the small indicators. private View mOnScreenIndicators; - // We use a thread in MediaSaver to do the work of saving images. This + // We use MediaSaveService to do the work of saving images in background. This // reduces the shot-to-shot time. - private MediaSaver mMediaSaver; - // Similarly, we use a thread to generate the name of the picture and insert - // it into MediaStore while picture taking is still in progress. + private MediaSaveService mMediaSaveService; + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder b) { + mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService(); + mMediaSaveService.setListener(PhotoModule.this); + } + @Override + public void onServiceDisconnected(ComponentName className) { + mMediaSaveService = null; + }}; + // We use a queue to generated names of the images to be used later + // when the image is ready to be saved. private NamedImages mNamedImages; private Runnable mDoSnapRunnable = new Runnable() { @@ -304,16 +318,16 @@ public class PhotoModule private PreviewGestures mGestures; - private MediaSaver.OnMediaSavedListener mOnMediaSavedListener = new MediaSaver.OnMediaSavedListener() { - @Override - - public void onMediaSaved(Uri uri) { - if (uri != null) { - mHandler.obtainMessage(UPDATE_SECURE_ALBUM_ITEM, uri).sendToTarget(); - Util.broadcastNewPicture(mActivity, uri); - } - } - }; + private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener = + new MediaSaveService.OnMediaSavedListener() { + @Override + public void onMediaSaved(Uri uri) { + if (uri != null) { + mActivity.addSecureAlbumItemIfNeeded(false, uri); + Util.broadcastNewPicture(mActivity, uri); + } + } + }; // The purpose is not to block the main thread in onCreate and onResume. private class CameraStartUpThread extends Thread { @@ -449,11 +463,6 @@ public class PhotoModule R.string.camera_disabled); break; } - - case UPDATE_SECURE_ALBUM_ITEM: { - mActivity.addSecureAlbumItemIfNeeded(false, (Uri) msg.obj); - break; - } } } } @@ -648,9 +657,15 @@ public class PhotoModule mShutterButton = mActivity.getShutterButton(); mShutterButton.setImageResource(R.drawable.btn_new_shutter); mShutterButton.setOnShutterButtonListener(this); + if (mMediaSaveService != null) { + if (mMediaSaveService.isQueueFull()) { + mShutterButton.enableTouch(false); + } else { + mShutterButton.enableTouch(true); + } + } mShutterButton.setVisibility(View.VISIBLE); - mMediaSaver = new MediaSaver(mContentResolver); mNamedImages = new NamedImages(); mFirstTimeInitialized = true; @@ -688,7 +703,6 @@ public class PhotoModule mPreferences, mContentResolver); mLocationManager.recordLocation(recordLocation); - mMediaSaver = new MediaSaver(mContentResolver); mNamedImages = new NamedImages(); initializeZoom(); keepMediaProviderInstance(); @@ -994,8 +1008,8 @@ public class PhotoModule Log.e(TAG, "Unbalanced name/data pair"); } else { if (date == -1) date = mCaptureStartTime; - mMediaSaver.addImage(jpegData, title, date, mLocation, width, height, - orientation, mOnMediaSavedListener); + mMediaSaveService.addImage(jpegData, title, date, mLocation, width, height, + orientation, mOnMediaSavedListener, mContentResolver); } } else { mJpegImageData = jpegData; @@ -1117,7 +1131,7 @@ public class PhotoModule // If we are already in the middle of taking a snapshot or the image save request // is full then ignore. if (mCameraDevice == null || mCameraState == SNAPSHOT_IN_PROGRESS - || mCameraState == SWITCHING_CAMERA || mMediaSaver.queueFull()) { + || mCameraState == SWITCHING_CAMERA || mMediaSaveService.isQueueFull()) { return false; } mCaptureStartTime = System.currentTimeMillis(); @@ -1517,6 +1531,7 @@ public class PhotoModule mCameraStartUpThread.start(); } + bindMediaSaveService(); // If first time initialization is not finished, put it in the // message queue. if (!mFirstTimeInitialized) { @@ -1530,6 +1545,18 @@ public class PhotoModule PopupManager.getInstance(mActivity).notifyShowPopup(null); } + private void bindMediaSaveService() { + Intent intent = new Intent(mActivity, MediaSaveService.class); + mActivity.startService(intent); // start service before binding it so the + // service won't be killed if we unbind it. + mActivity.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + private void unbindMediaSaveService() { + mMediaSaveService.setListener(null); + mActivity.unbindService(mConnection); + } + void waitCameraStartUpThread() { try { if (mCameraStartUpThread != null) { @@ -1576,18 +1603,13 @@ public class PhotoModule mSurfaceTexture = null; } resetScreenOn(); + unbindMediaSaveService(); // Clear UI. collapseCameraControls(); if (mFaceView != null) mFaceView.clear(); - if (mFirstTimeInitialized) { - if (mMediaSaver != null) { - mMediaSaver.finish(); - mMediaSaver = null; - mNamedImages = null; - } - } + mNamedImages = null; if (mLocationManager != null) mLocationManager.recordLocation(false); @@ -2478,4 +2500,13 @@ public class PhotoModule } } + @Override + public void onQueueAvailable() { + if (mShutterButton != null) mShutterButton.enableTouch(true); + } + + @Override + public void onQueueFull() { + if (mShutterButton != null) mShutterButton.enableTouch(false); + } } -- cgit v1.2.3