CaptureSessionImpl.java revision dae7a41623e04e8d583dfd636e3f5a5ae0e245e6
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera.session;
18
19import android.graphics.Bitmap;
20import android.graphics.BitmapFactory;
21import android.location.Location;
22import android.net.Uri;
23import android.os.AsyncTask;
24
25import com.android.camera.app.MediaSaver;
26import com.android.camera.data.FilmstripItemData;
27import com.android.camera.debug.Log;
28import com.android.camera.exif.ExifInterface;
29import com.android.camera.stats.CaptureSessionStatsCollector;
30import com.android.camera.util.FileUtil;
31import com.android.camera.util.Size;
32import com.google.common.base.Optional;
33import com.google.common.util.concurrent.ListenableFuture;
34import com.google.common.util.concurrent.SettableFuture;
35
36import java.io.File;
37import java.io.IOException;
38import java.util.HashSet;
39
40import javax.annotation.Nonnull;
41import javax.annotation.Nullable;
42
43/**
44 * The default implementation of the CaptureSession interface. This is the
45 * implementation we use for normal Camera use.
46 */
47public class CaptureSessionImpl implements CaptureSession {
48    private static final Log.Tag TAG = new Log.Tag("CaptureSessionImpl");
49
50    /** The capture session manager responsible for this session. */
51    private final CaptureSessionManager mSessionManager;
52    /** Used to inform about session status updates. */
53    private final SessionNotifier mSessionNotifier;
54    /** Used for adding/removing/updating placeholders for in-progress sessions. */
55    private final PlaceholderManager mPlaceholderManager;
56    /** A place holder for this capture session. */
57    private PlaceholderManager.Placeholder mPlaceHolder;
58    /** Used to store images on disk and to add them to the media store. */
59    private final MediaSaver mMediaSaver;
60    /** The title of the item being processed. */
61    private final String mTitle;
62    /** These listeners get informed about progress updates. */
63    private final HashSet<ProgressListener> mProgressListeners = new HashSet<>();
64    private final long mSessionStartMillis;
65    /**
66     * The file that can be used to write the final JPEG output temporarily,
67     * before it is copied to the final location.
68     */
69    private final TemporarySessionFile mTempOutputFile;
70    /** Saver that is used to store a stack of images. */
71    private final StackSaver mStackSaver;
72    /** A URI of the item being processed. */
73    private Uri mUri;
74    /** The location this session was created at. Used for media store. */
75    private Location mLocation;
76    /** The current progress of this session in percent. */
77    private int mProgressPercent = 0;
78    /** A message ID for the current progress state. */
79    private int mProgressMessageId;
80    private Uri mContentUri;
81    /** Whether this image was finished. */
82    private volatile boolean mIsFinished;
83    /** Object that collects logging information through the capture session lifecycle */
84    private final CaptureSessionStatsCollector mCaptureSessionStatsCollector = new CaptureSessionStatsCollector();
85
86    @Nullable
87    private ImageLifecycleListener mImageLifecycleListener;
88    private boolean mHasPreviouslySetProgress = false;
89
90    /**
91     * Creates a new {@link CaptureSession}.
92     *
93     * @param title the title of this session.
94     * @param sessionStartMillis the timestamp of this capture session (since
95     *            epoch).
96     * @param location the location of this session, used for media store.
97     * @param temporarySessionFile used to create a temporary session file if
98     *            necessary.
99     * @param captureSessionManager the capture session manager responsible for
100     *            this session.
101     * @param placeholderManager used to add/update/remove session placeholders.
102     * @param mediaSaver used to store images on disk and add them to the media
103     *            store.
104     * @param stackSaver used to save stacks of images that belong to this
105     *            session.
106     */
107    /* package */CaptureSessionImpl(String title,
108            long sessionStartMillis, Location location, TemporarySessionFile temporarySessionFile,
109            CaptureSessionManager captureSessionManager, SessionNotifier sessionNotifier,
110            PlaceholderManager placeholderManager, MediaSaver mediaSaver, StackSaver stackSaver) {
111        mTitle = title;
112        mSessionStartMillis = sessionStartMillis;
113        mLocation = location;
114        mTempOutputFile = temporarySessionFile;
115        mSessionManager = captureSessionManager;
116        mSessionNotifier = sessionNotifier;
117        mPlaceholderManager = placeholderManager;
118        mMediaSaver = mediaSaver;
119        mStackSaver = stackSaver;
120        mIsFinished = false;
121    }
122
123    @Override
124    public CaptureSessionStatsCollector getCollector() {
125        return mCaptureSessionStatsCollector;
126    }
127
128    @Override
129    public String getTitle() {
130        return mTitle;
131    }
132
133    @Override
134    public Location getLocation() {
135        return mLocation;
136    }
137
138    @Override
139    public void setLocation(Location location) {
140        mLocation = location;
141    }
142
143    @Override
144    public synchronized int getProgress() {
145        return mProgressPercent;
146    }
147
148    @Override
149    public synchronized void setProgress(int percent) {
150        if (!mHasPreviouslySetProgress && percent == 0 && mImageLifecycleListener != null) {
151            mImageLifecycleListener.onProcessingStarted();
152        }
153
154        mProgressPercent = percent;
155        mSessionNotifier.notifyTaskProgress(mUri, mProgressPercent);
156        for (ProgressListener listener : mProgressListeners) {
157            listener.onProgressChanged(percent);
158        }
159    }
160
161    @Override
162    public synchronized int getProgressMessageId() {
163        return mProgressMessageId;
164    }
165
166    @Override
167    public synchronized void setProgressMessage(int messageId) {
168        mProgressMessageId = messageId;
169        mSessionNotifier.notifyTaskProgressText(mUri, messageId);
170        for (ProgressListener listener : mProgressListeners) {
171            listener.onStatusMessageChanged(messageId);
172        }
173    }
174
175    @Override
176    public void updateThumbnail(Bitmap bitmap) {
177        // No placeholder present means the task might already be finished or
178        // cancelled.
179        if (mPlaceHolder == null) {
180            return;
181        }
182        if (mImageLifecycleListener != null) {
183            mImageLifecycleListener.onMediumThumb();
184        }
185        mPlaceholderManager.replacePlaceholder(mPlaceHolder, bitmap);
186        mSessionNotifier.notifySessionUpdated(mUri);
187    }
188
189    @Override
190    public void updateCaptureIndicatorThumbnail(Bitmap indicator, int rotationDegrees) {
191        if (mImageLifecycleListener != null) {
192            mImageLifecycleListener.onTinyThumb();
193        }
194        onCaptureIndicatorUpdate(indicator, rotationDegrees);
195
196    }
197
198    @Override
199    public synchronized void startEmpty(@Nullable ImageLifecycleListener listener,
200          @Nonnull Size pictureSize) {
201        if (mIsFinished) {
202            return;
203        }
204
205        if (listener != null) {
206            mImageLifecycleListener = listener;
207            mImageLifecycleListener.onCaptureStarted();
208        }
209
210        mProgressMessageId = -1;
211        mPlaceHolder = mPlaceholderManager.insertEmptyPlaceholder(mTitle, pictureSize,
212                mSessionStartMillis);
213        mUri = mPlaceHolder.outputUri;
214        mSessionManager.putSession(mUri, this);
215        mSessionNotifier.notifyTaskQueued(mUri);
216    }
217
218    @Override
219    public synchronized void startSession(@Nullable ImageLifecycleListener listener,
220          @Nonnull Bitmap placeholder, int progressMessageId) {
221        if (mIsFinished) {
222            return;
223        }
224
225        if (listener != null) {
226            mImageLifecycleListener = listener;
227            mImageLifecycleListener.onCaptureStarted();
228        }
229
230        mProgressMessageId = progressMessageId;
231        mPlaceHolder = mPlaceholderManager.insertPlaceholder(mTitle, placeholder,
232                mSessionStartMillis);
233        mUri = mPlaceHolder.outputUri;
234        mSessionManager.putSession(mUri, this);
235        mSessionNotifier.notifyTaskQueued(mUri);
236        onCaptureIndicatorUpdate(placeholder, 0);
237    }
238
239    @Override
240    public synchronized void startSession(@Nullable ImageLifecycleListener listener,
241          @Nonnull byte[] placeholder, int progressMessageId) {
242        if (mIsFinished) {
243            return;
244        }
245
246        if (listener != null) {
247            mImageLifecycleListener = listener;
248            mImageLifecycleListener.onCaptureStarted();
249        }
250
251        mProgressMessageId = progressMessageId;
252        mPlaceHolder = mPlaceholderManager.insertPlaceholder(mTitle, placeholder,
253                mSessionStartMillis);
254        mUri = mPlaceHolder.outputUri;
255        mSessionManager.putSession(mUri, this);
256        mSessionNotifier.notifyTaskQueued(mUri);
257        Optional<Bitmap> placeholderBitmap =
258                mPlaceholderManager.getPlaceholder(mPlaceHolder);
259        if (placeholderBitmap.isPresent()) {
260            onCaptureIndicatorUpdate(placeholderBitmap.get(), 0);
261        }
262    }
263
264    @Override
265    public synchronized void startSession(@Nullable ImageLifecycleListener listener,
266          @Nonnull Uri uri, int progressMessageId) {
267        if (listener != null) {
268            mImageLifecycleListener = listener;
269            mImageLifecycleListener.onCaptureStarted();
270        }
271
272        mUri = uri;
273        mProgressMessageId = progressMessageId;
274        mPlaceHolder = mPlaceholderManager.convertToPlaceholder(uri);
275
276        mSessionManager.putSession(mUri, this);
277        mSessionNotifier.notifyTaskQueued(mUri);
278    }
279
280    @Override
281    public synchronized void cancel() {
282        if (isStarted()) {
283            mSessionManager.removeSession(mUri);
284            mSessionNotifier.notifyTaskCanceled(mUri);
285        }
286
287        if (mPlaceHolder != null) {
288            mPlaceholderManager.removePlaceholder(mPlaceHolder);
289            mPlaceHolder = null;
290        }
291    }
292
293    @Override
294    public synchronized ListenableFuture<Optional<Uri>> saveAndFinish(byte[] data, int width,
295          int height, int orientation, ExifInterface exif) {
296        final SettableFuture<Optional<Uri>> futureResult = SettableFuture.create();
297
298        if (mImageLifecycleListener != null) {
299            mImageLifecycleListener.onProcessingComplete();
300        }
301
302        mIsFinished = true;
303        if (mPlaceHolder == null) {
304
305            mMediaSaver.addImage(
306                    data, mTitle, mSessionStartMillis, mLocation, width, height,
307                    orientation, exif, new MediaSaver.OnMediaSavedListener() {
308                        @Override
309                        public void onMediaSaved(Uri uri) {
310                            futureResult.set(Optional.fromNullable(uri));
311
312                            if (mImageLifecycleListener != null) {
313                                mImageLifecycleListener.onCapturePersisted();
314                            }
315                        }
316                    });
317        } else {
318            try {
319                mContentUri = mPlaceholderManager.finishPlaceholder(mPlaceHolder, mLocation,
320                        orientation, exif, data, width, height, FilmstripItemData.MIME_TYPE_JPEG);
321                mSessionNotifier.notifyTaskDone(mUri);
322                futureResult.set(Optional.fromNullable(mUri));
323
324                if (mImageLifecycleListener != null) {
325                    mImageLifecycleListener.onCapturePersisted();
326                }
327            } catch (IOException e) {
328                Log.e(TAG, "Could not write file", e);
329                finishWithFailure(-1, true);
330                futureResult.setException(e);
331            }
332        }
333        return futureResult;
334    }
335
336    @Override
337    public StackSaver getStackSaver() {
338        return mStackSaver;
339    }
340
341    @Override
342    public void finish() {
343        if (mPlaceHolder == null) {
344            throw new IllegalStateException(
345                    "Cannot call finish without calling startSession first.");
346        }
347
348        mIsFinished = true;
349        AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
350            @Override
351            public void run() {
352                byte[] jpegDataTemp;
353                if (mTempOutputFile.isUsable()) {
354                    try {
355                        jpegDataTemp = FileUtil.readFileToByteArray(mTempOutputFile.getFile());
356                    } catch (IOException e) {
357                        return;
358                    }
359                } else {
360                    return;
361                }
362                final byte[] jpegData = jpegDataTemp;
363
364                BitmapFactory.Options options = new BitmapFactory.Options();
365                options.inJustDecodeBounds = true;
366                BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, options);
367                int width = options.outWidth;
368                int height = options.outHeight;
369                int rotation = 0;
370                ExifInterface exif = null;
371                try {
372                    exif = new ExifInterface();
373                    exif.readExif(jpegData);
374                } catch (IOException e) {
375                    Log.w(TAG, "Could not read exif", e);
376                    exif = null;
377                }
378                CaptureSessionImpl.this.saveAndFinish(jpegData, width, height, rotation, exif);
379            }
380        });
381
382    }
383
384    @Override
385    public TemporarySessionFile getTempOutputFile() {
386        return mTempOutputFile;
387    }
388
389    @Override
390    public Uri getUri() {
391        return mUri;
392    }
393
394    @Override
395    public void updatePreview() {
396        final File path;
397        if (mTempOutputFile.isUsable()) {
398            path = mTempOutputFile.getFile();
399        } else {
400            Log.e(TAG, "Cannot update preview");
401            return;
402        }
403        AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
404            @Override
405            public void run() {
406                byte[] jpegDataTemp;
407                try {
408                    jpegDataTemp = FileUtil.readFileToByteArray(path);
409                } catch (IOException e) {
410                    return;
411                }
412                final byte[] jpegData = jpegDataTemp;
413
414                BitmapFactory.Options options = new BitmapFactory.Options();
415                Bitmap placeholder = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length,
416                        options);
417                mPlaceholderManager.replacePlaceholder(mPlaceHolder, placeholder);
418                mSessionNotifier.notifySessionUpdated(mUri);
419            }
420        });
421    }
422
423    @Override
424    public void finishWithFailure(int failureMessageId, boolean removeFromFilmstrip) {
425        if (mPlaceHolder == null) {
426            throw new IllegalStateException(
427                    "Cannot call finish without calling startSession first.");
428        }
429        mProgressMessageId = failureMessageId;
430        mSessionManager.putErrorMessage(mUri, failureMessageId);
431        mSessionNotifier.notifyTaskFailed(mUri, failureMessageId, removeFromFilmstrip);
432    }
433
434    @Override
435    public void addProgressListener(ProgressListener listener) {
436        if (mProgressMessageId > 0) {
437            listener.onStatusMessageChanged(mProgressMessageId);
438        }
439        listener.onProgressChanged(mProgressPercent);
440        mProgressListeners.add(listener);
441    }
442
443    @Override
444    public void removeProgressListener(ProgressListener listener) {
445        mProgressListeners.remove(listener);
446    }
447
448    @Override
449    public void finalizeSession() {
450        mPlaceholderManager.removePlaceholder(mPlaceHolder);
451    }
452
453    private void onCaptureIndicatorUpdate(Bitmap indicator, int rotationDegrees) {
454        mSessionNotifier.notifySessionCaptureIndicatorAvailable(indicator, rotationDegrees);
455    }
456
457    private boolean isStarted() {
458        return mUri != null;
459    }
460}
461