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            if (mImageLifecycleListener != null) {
286                mImageLifecycleListener.onCaptureCanceled();
287            }
288        }
289
290        if (mPlaceHolder != null) {
291            mPlaceholderManager.removePlaceholder(mPlaceHolder);
292            mPlaceHolder = null;
293        }
294    }
295
296    @Override
297    public synchronized ListenableFuture<Optional<Uri>> saveAndFinish(byte[] data, int width,
298          int height, int orientation, ExifInterface exif) {
299        final SettableFuture<Optional<Uri>> futureResult = SettableFuture.create();
300
301        if (mImageLifecycleListener != null) {
302            mImageLifecycleListener.onProcessingComplete();
303        }
304
305        mIsFinished = true;
306        if (mPlaceHolder == null) {
307
308            mMediaSaver.addImage(
309                    data, mTitle, mSessionStartMillis, mLocation, width, height,
310                    orientation, exif, new MediaSaver.OnMediaSavedListener() {
311                        @Override
312                        public void onMediaSaved(Uri uri) {
313                            futureResult.set(Optional.fromNullable(uri));
314
315                            if (mImageLifecycleListener != null) {
316                                mImageLifecycleListener.onCapturePersisted();
317                            }
318                        }
319                    });
320        } else {
321            try {
322                mContentUri = mPlaceholderManager.finishPlaceholder(mPlaceHolder, mLocation,
323                        orientation, exif, data, width, height, FilmstripItemData.MIME_TYPE_JPEG);
324                mSessionNotifier.notifyTaskDone(mUri);
325                futureResult.set(Optional.fromNullable(mUri));
326
327                if (mImageLifecycleListener != null) {
328                    mImageLifecycleListener.onCapturePersisted();
329                }
330            } catch (IOException e) {
331                Log.e(TAG, "Could not write file", e);
332                if (mImageLifecycleListener != null) {
333                    mImageLifecycleListener.onCaptureFailed();
334                }
335                finishWithFailure(-1, true);
336                futureResult.setException(e);
337            }
338        }
339        return futureResult;
340    }
341
342    @Override
343    public StackSaver getStackSaver() {
344        return mStackSaver;
345    }
346
347    @Override
348    public void finish() {
349        if (mPlaceHolder == null) {
350            throw new IllegalStateException(
351                    "Cannot call finish without calling startSession first.");
352        }
353
354        mIsFinished = true;
355        AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
356            @Override
357            public void run() {
358                byte[] jpegDataTemp;
359                if (mTempOutputFile.isUsable()) {
360                    try {
361                        jpegDataTemp = FileUtil.readFileToByteArray(mTempOutputFile.getFile());
362                    } catch (IOException e) {
363                        return;
364                    }
365                } else {
366                    return;
367                }
368                final byte[] jpegData = jpegDataTemp;
369
370                BitmapFactory.Options options = new BitmapFactory.Options();
371                options.inJustDecodeBounds = true;
372                BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, options);
373                int width = options.outWidth;
374                int height = options.outHeight;
375                int rotation = 0;
376                ExifInterface exif = null;
377                try {
378                    exif = new ExifInterface();
379                    exif.readExif(jpegData);
380                } catch (IOException e) {
381                    Log.w(TAG, "Could not read exif", e);
382                    exif = null;
383                }
384                CaptureSessionImpl.this.saveAndFinish(jpegData, width, height, rotation, exif);
385            }
386        });
387
388    }
389
390    @Override
391    public TemporarySessionFile getTempOutputFile() {
392        return mTempOutputFile;
393    }
394
395    @Override
396    public Uri getUri() {
397        return mUri;
398    }
399
400    @Override
401    public void updatePreview() {
402        final File path;
403        if (mTempOutputFile.isUsable()) {
404            path = mTempOutputFile.getFile();
405        } else {
406            Log.e(TAG, "Cannot update preview");
407            return;
408        }
409        AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
410            @Override
411            public void run() {
412                byte[] jpegDataTemp;
413                try {
414                    jpegDataTemp = FileUtil.readFileToByteArray(path);
415                } catch (IOException e) {
416                    return;
417                }
418                final byte[] jpegData = jpegDataTemp;
419
420                BitmapFactory.Options options = new BitmapFactory.Options();
421                Bitmap placeholder = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length,
422                        options);
423                mPlaceholderManager.replacePlaceholder(mPlaceHolder, placeholder);
424                mSessionNotifier.notifySessionUpdated(mUri);
425            }
426        });
427    }
428
429    @Override
430    public void finishWithFailure(int failureMessageId, boolean removeFromFilmstrip) {
431        if (mPlaceHolder == null) {
432            throw new IllegalStateException(
433                    "Cannot call finish without calling startSession first.");
434        }
435        mProgressMessageId = failureMessageId;
436        mSessionManager.putErrorMessage(mUri, failureMessageId);
437        mSessionNotifier.notifyTaskFailed(mUri, failureMessageId, removeFromFilmstrip);
438    }
439
440    @Override
441    public void addProgressListener(ProgressListener listener) {
442        if (mProgressMessageId > 0) {
443            listener.onStatusMessageChanged(mProgressMessageId);
444        }
445        listener.onProgressChanged(mProgressPercent);
446        mProgressListeners.add(listener);
447    }
448
449    @Override
450    public void removeProgressListener(ProgressListener listener) {
451        mProgressListeners.remove(listener);
452    }
453
454    @Override
455    public void finalizeSession() {
456        mPlaceholderManager.removePlaceholder(mPlaceHolder);
457    }
458
459    private void onCaptureIndicatorUpdate(Bitmap indicator, int rotationDegrees) {
460        mSessionNotifier.notifySessionCaptureIndicatorAvailable(indicator, rotationDegrees);
461    }
462
463    private boolean isStarted() {
464        return mUri != null;
465    }
466}
467