android:theme="@style/Theme.ProxyLauncher">
</activity>
<service android:name="com.android.gallery3d.app.BatchService" />
+ <service android:name="com.android.camera.MediaSaveService" />
<!-- canvas -->
<provider
android:name="com.android.gallery3d.provider.CanvasProvider"
--- /dev/null
+/*
+ * 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 <Void, Void, Uri> {
+ 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();
+ }
+ }
+}
+++ /dev/null
-/*
- * 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<SaveRequest> mQueue;
- private boolean mStop;
- private ContentResolver mContentResolver;
-
- public interface OnMediaSavedListener {
- public void onMediaSaved(Uri uri);
- }
-
- public MediaSaver(ContentResolver resolver) {
- mContentResolver = resolver;
- mQueue = new ArrayList<SaveRequest>();
- 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;
- }
-}
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;
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;
ShutterButton.OnShutterButtonListener,
SurfaceHolder.Callback,
PieRenderer.PieListener,
- CountDownView.OnCountDownFinishedListener {
+ CountDownView.OnCountDownFinishedListener,
+ MediaSaveService.Listener {
private static final String TAG = "CAM_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;
// 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() {
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 {
R.string.camera_disabled);
break;
}
-
- case UPDATE_SECURE_ALBUM_ITEM: {
- mActivity.addSecureAlbumItemIfNeeded(false, (Uri) msg.obj);
- break;
- }
}
}
}
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;
mPreferences, mContentResolver);
mLocationManager.recordLocation(recordLocation);
- mMediaSaver = new MediaSaver(mContentResolver);
mNamedImages = new NamedImages();
initializeZoom();
keepMediaProviderInstance();
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;
// 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();
mCameraStartUpThread.start();
}
+ bindMediaSaveService();
// If first time initialization is not finished, put it in the
// message queue.
if (!mFirstTimeInitialized) {
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) {
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);
}
}
+ @Override
+ public void onQueueAvailable() {
+ if (mShutterButton != null) mShutterButton.enableTouch(true);
+ }
+
+ @Override
+ public void onQueueFull() {
+ if (mShutterButton != null) mShutterButton.enableTouch(false);
+ }
}