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 17 package com.android.camera.session; 18 19 import android.graphics.Bitmap; 20 import android.graphics.BitmapFactory; 21 import android.location.Location; 22 import android.net.Uri; 23 import android.os.AsyncTask; 24 25 import com.android.camera.app.MediaSaver; 26 import com.android.camera.data.FilmstripItemData; 27 import com.android.camera.debug.Log; 28 import com.android.camera.exif.ExifInterface; 29 import com.android.camera.stats.CaptureSessionStatsCollector; 30 import com.android.camera.util.FileUtil; 31 import com.android.camera.util.Size; 32 import com.google.common.base.Optional; 33 import com.google.common.util.concurrent.ListenableFuture; 34 import com.google.common.util.concurrent.SettableFuture; 35 36 import java.io.File; 37 import java.io.IOException; 38 import java.util.HashSet; 39 40 import javax.annotation.Nonnull; 41 import 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 */ 47 public 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 */ CaptureSessionImpl(String title, long sessionStartMillis, Location location, TemporarySessionFile temporarySessionFile, CaptureSessionManager captureSessionManager, SessionNotifier sessionNotifier, PlaceholderManager placeholderManager, MediaSaver mediaSaver, StackSaver stackSaver)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 getCollector()124 public CaptureSessionStatsCollector getCollector() { 125 return mCaptureSessionStatsCollector; 126 } 127 128 @Override getTitle()129 public String getTitle() { 130 return mTitle; 131 } 132 133 @Override getLocation()134 public Location getLocation() { 135 return mLocation; 136 } 137 138 @Override setLocation(Location location)139 public void setLocation(Location location) { 140 mLocation = location; 141 } 142 143 @Override getProgress()144 public synchronized int getProgress() { 145 return mProgressPercent; 146 } 147 148 @Override setProgress(int percent)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 getProgressMessageId()162 public synchronized int getProgressMessageId() { 163 return mProgressMessageId; 164 } 165 166 @Override setProgressMessage(int messageId)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 updateThumbnail(Bitmap bitmap)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 updateCaptureIndicatorThumbnail(Bitmap indicator, int rotationDegrees)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 startEmpty(@ullable ImageLifecycleListener listener, @Nonnull Size pictureSize)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 startSession(@ullable ImageLifecycleListener listener, @Nonnull Bitmap placeholder, int progressMessageId)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 startSession(@ullable ImageLifecycleListener listener, @Nonnull byte[] placeholder, int progressMessageId)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 startSession(@ullable ImageLifecycleListener listener, @Nonnull Uri uri, int progressMessageId)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 cancel()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 saveAndFinish(byte[] data, int width, int height, int orientation, ExifInterface exif)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 getStackSaver()343 public StackSaver getStackSaver() { 344 return mStackSaver; 345 } 346 347 @Override finish()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 getTempOutputFile()391 public TemporarySessionFile getTempOutputFile() { 392 return mTempOutputFile; 393 } 394 395 @Override getUri()396 public Uri getUri() { 397 return mUri; 398 } 399 400 @Override updatePreview()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 finishWithFailure(int failureMessageId, boolean removeFromFilmstrip)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 addProgressListener(ProgressListener listener)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 removeProgressListener(ProgressListener listener)450 public void removeProgressListener(ProgressListener listener) { 451 mProgressListeners.remove(listener); 452 } 453 454 @Override finalizeSession()455 public void finalizeSession() { 456 mPlaceholderManager.removePlaceholder(mPlaceHolder); 457 } 458 onCaptureIndicatorUpdate(Bitmap indicator, int rotationDegrees)459 private void onCaptureIndicatorUpdate(Bitmap indicator, int rotationDegrees) { 460 mSessionNotifier.notifySessionCaptureIndicatorAvailable(indicator, rotationDegrees); 461 } 462 isStarted()463 private boolean isStarted() { 464 return mUri != null; 465 } 466 } 467