1 /* 2 * Copyright (C) 2013 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.content.ContentResolver; 20 import android.graphics.BitmapFactory; 21 import android.location.Location; 22 import android.net.Uri; 23 import android.os.AsyncTask; 24 import android.os.Handler; 25 import android.os.Looper; 26 27 import com.android.camera.app.MediaSaver; 28 import com.android.camera.app.MediaSaver.OnMediaSavedListener; 29 import com.android.camera.data.LocalData; 30 import com.android.camera.debug.Log; 31 import com.android.camera.exif.ExifInterface; 32 import com.android.camera.util.FileUtil; 33 34 import java.io.File; 35 import java.io.IOException; 36 import java.util.HashMap; 37 import java.util.HashSet; 38 import java.util.LinkedList; 39 import java.util.Map; 40 41 /** 42 * Implementation for the {@link CaptureSessionManager}. 43 */ 44 public class CaptureSessionManagerImpl implements CaptureSessionManager { 45 46 private static final Log.Tag TAG = new Log.Tag("CaptureSessMgrImpl"); 47 public static final String TEMP_SESSIONS = "TEMP_SESSIONS"; 48 49 private class CaptureSessionImpl implements CaptureSession { 50 /** A URI of the item being processed. */ 51 private Uri mUri; 52 /** The title of the item being processed. */ 53 private final String mTitle; 54 /** The location this session was created at. Used for media store.*/ 55 private Location mLocation; 56 /** The current progress of this session in percent. */ 57 private int mProgressPercent = 0; 58 /** A message ID for the current progress state. */ 59 private CharSequence mProgressMessage; 60 /** A place holder for this capture session. */ 61 private PlaceholderManager.Session mPlaceHolderSession; 62 private boolean mNoPlaceHolderRequired = false; 63 private Uri mContentUri; 64 /** These listeners get informed about progress updates. */ 65 private final HashSet<ProgressListener> mProgressListeners = 66 new HashSet<ProgressListener>(); 67 private final long mSessionStartMillis; 68 69 /** 70 * Creates a new {@link CaptureSession}. 71 * 72 * @param title the title of this session. 73 * @param sessionStartMillis the timestamp of this capture session (since epoch). 74 * @param location the location of this session, used for media store. 75 */ 76 private CaptureSessionImpl(String title, long sessionStartMillis, Location location) { 77 mTitle = title; 78 mSessionStartMillis = sessionStartMillis; 79 mLocation = location; 80 } 81 82 @Override 83 public String getTitle() { 84 return mTitle; 85 } 86 87 @Override 88 public Location getLocation() { 89 return mLocation; 90 } 91 92 @Override 93 public void setLocation(Location location) { 94 mLocation = location; 95 } 96 97 @Override 98 public synchronized void setProgress(int percent) { 99 mProgressPercent = percent; 100 notifyTaskProgress(mUri, mProgressPercent); 101 for (ProgressListener listener : mProgressListeners) { 102 listener.onProgressChanged(percent); 103 } 104 } 105 106 @Override 107 public synchronized int getProgress() { 108 return mProgressPercent; 109 } 110 111 @Override 112 public synchronized CharSequence getProgressMessage() { 113 return mProgressMessage; 114 } 115 116 @Override 117 public synchronized void setProgressMessage(CharSequence message) { 118 mProgressMessage = message; 119 notifyTaskProgressText(mUri, message); 120 for (ProgressListener listener : mProgressListeners) { 121 listener.onStatusMessageChanged(message); 122 } 123 } 124 125 @Override 126 public void startEmpty() { 127 mNoPlaceHolderRequired = true; 128 } 129 130 @Override 131 public synchronized void startSession(byte[] placeholder, CharSequence progressMessage) { 132 mProgressMessage = progressMessage; 133 134 // TODO: This needs to happen outside the UI thread. 135 mPlaceHolderSession = mPlaceholderManager.insertPlaceholder(mTitle, placeholder, 136 mSessionStartMillis); 137 mUri = mPlaceHolderSession.outputUri; 138 putSession(mUri, this); 139 notifyTaskQueued(mUri); 140 } 141 142 @Override 143 public synchronized void startSession(Uri uri, CharSequence progressMessage) { 144 mUri = uri; 145 mProgressMessage = progressMessage; 146 mPlaceHolderSession = mPlaceholderManager.convertToPlaceholder(uri); 147 148 mSessions.put(mUri.toString(), this); 149 notifyTaskQueued(mUri); 150 } 151 152 @Override 153 public synchronized void cancel() { 154 if (mUri != null) { 155 removeSession(mUri.toString()); 156 } 157 } 158 159 @Override 160 public synchronized void saveAndFinish(byte[] data, int width, int height, int orientation, 161 ExifInterface exif, final OnMediaSavedListener listener) { 162 if (mNoPlaceHolderRequired) { 163 mMediaSaver.addImage( 164 data, mTitle, mSessionStartMillis, null, width, height, 165 orientation, exif, listener, mContentResolver); 166 return; 167 } 168 169 if (mPlaceHolderSession == null) { 170 throw new IllegalStateException( 171 "Cannot call saveAndFinish without calling startSession first."); 172 } 173 174 // TODO: This needs to happen outside the UI thread. 175 mContentUri = mPlaceholderManager.finishPlaceholder(mPlaceHolderSession, mLocation, 176 orientation, exif, data, width, height, LocalData.MIME_TYPE_JPEG); 177 178 removeSession(mUri.toString()); 179 notifyTaskDone(mPlaceHolderSession.outputUri); 180 } 181 182 @Override 183 public void finish() { 184 if (mPlaceHolderSession == null) { 185 throw new IllegalStateException( 186 "Cannot call finish without calling startSession first."); 187 } 188 189 final String path = this.getPath(); 190 191 AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() { 192 @Override 193 public void run() { 194 byte[] jpegDataTemp; 195 try { 196 jpegDataTemp = FileUtil.readFileToByteArray(new File(path)); 197 } catch (IOException e) { 198 return; 199 } 200 final byte[] jpegData = jpegDataTemp; 201 202 BitmapFactory.Options options = new BitmapFactory.Options(); 203 options.inJustDecodeBounds = true; 204 BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, options); 205 int width = options.outWidth; 206 int height = options.outHeight; 207 int rotation = 0; 208 ExifInterface exif = null; 209 try { 210 exif = new ExifInterface(); 211 exif.readExif(jpegData); 212 } catch (IOException e) { 213 Log.w(TAG, "Could not read exif", e); 214 exif = null; 215 } 216 CaptureSessionImpl.this.saveAndFinish(jpegData, width, height, rotation, exif, 217 null); 218 } 219 }); 220 221 } 222 223 @Override 224 public String getPath() { 225 if (mUri == null) { 226 throw new IllegalStateException("Cannot retrieve URI of not started session."); 227 } 228 229 File tempDirectory = null; 230 try { 231 tempDirectory = new File( 232 getSessionDirectory(TEMP_SESSIONS), mTitle); 233 } catch (IOException e) { 234 Log.e(TAG, "Could not get temp session directory", e); 235 throw new RuntimeException("Could not get temp session directory", e); 236 } 237 tempDirectory.mkdirs(); 238 File tempFile = new File(tempDirectory, mTitle + ".jpg"); 239 try { 240 if (!tempFile.exists()) { 241 tempFile.createNewFile(); 242 } 243 } catch (IOException e) { 244 Log.e(TAG, "Could not create temp session file", e); 245 throw new RuntimeException("Could not create temp session file", e); 246 } 247 return tempFile.getPath(); 248 } 249 250 @Override 251 public Uri getUri() { 252 return mUri; 253 } 254 255 @Override 256 public Uri getContentUri() { 257 return mContentUri; 258 } 259 260 @Override 261 public boolean hasPath() { 262 return mUri != null; 263 } 264 265 @Override 266 public void onPreviewAvailable() { 267 notifySessionPreviewAvailable(mPlaceHolderSession.outputUri); 268 } 269 270 @Override 271 public void updatePreview(String previewPath) { 272 273 final String path = this.getPath(); 274 275 AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() { 276 @Override 277 public void run() { 278 byte[] jpegDataTemp; 279 try { 280 jpegDataTemp = FileUtil.readFileToByteArray(new File(path)); 281 } catch (IOException e) { 282 return; 283 } 284 final byte[] jpegData = jpegDataTemp; 285 286 BitmapFactory.Options options = new BitmapFactory.Options(); 287 options.inJustDecodeBounds = true; 288 BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, options); 289 int width = options.outWidth; 290 int height = options.outHeight; 291 292 mPlaceholderManager.replacePlaceholder(mPlaceHolderSession, jpegData, width, height); 293 onPreviewAvailable(); 294 } 295 }); 296 } 297 298 @Override 299 public void finishWithFailure(CharSequence reason) { 300 if (mPlaceHolderSession == null) { 301 throw new IllegalStateException( 302 "Cannot call finish without calling startSession first."); 303 } 304 mProgressMessage = reason; 305 306 removeSession(mUri.toString()); 307 mFailedSessionMessages.put(mPlaceHolderSession.outputUri, reason); 308 notifyTaskFailed(mPlaceHolderSession.outputUri, reason); 309 } 310 311 @Override 312 public void addProgressListener(ProgressListener listener) { 313 listener.onStatusMessageChanged(mProgressMessage); 314 listener.onProgressChanged(mProgressPercent); 315 mProgressListeners.add(listener); 316 } 317 318 @Override 319 public void removeProgressListener(ProgressListener listener) { 320 mProgressListeners.remove(listener); 321 } 322 } 323 324 private final MediaSaver mMediaSaver; 325 private final PlaceholderManager mPlaceholderManager; 326 private final SessionStorageManager mSessionStorageManager; 327 private final ContentResolver mContentResolver; 328 329 /** Failed session messages. Uri -> message. */ 330 private final HashMap<Uri, CharSequence> mFailedSessionMessages = 331 new HashMap<Uri, CharSequence>(); 332 333 /** 334 * We use this to fire events to the session listeners from the main thread. 335 */ 336 private final Handler mMainHandler = new Handler(Looper.getMainLooper()); 337 338 /** Sessions in progress, keyed by URI. */ 339 private final Map<String, CaptureSession> mSessions; 340 341 /** Listeners interested in task update events. */ 342 private final LinkedList<SessionListener> mTaskListeners = new LinkedList<SessionListener>(); 343 344 /** 345 * Initializes a new {@link CaptureSessionManager} implementation. 346 * 347 * @param mediaSaver used to store the resulting media item 348 * @param contentResolver required by the media saver 349 * @param placeholderManager used to manage placeholders in the filmstrip 350 * before the final result is ready 351 * @param sessionStorageManager used to tell modules where to store 352 * temporary session data 353 */ 354 public CaptureSessionManagerImpl(MediaSaver mediaSaver, ContentResolver contentResolver, 355 PlaceholderManager placeholderManager, SessionStorageManager sessionStorageManager) { 356 mSessions = new HashMap<String, CaptureSession>(); 357 mMediaSaver = mediaSaver; 358 mContentResolver = contentResolver; 359 mPlaceholderManager = placeholderManager; 360 mSessionStorageManager = sessionStorageManager; 361 } 362 363 @Override 364 public CaptureSession createNewSession(String title, long sessionStartTime, Location location) { 365 return new CaptureSessionImpl(title, sessionStartTime, location); 366 } 367 368 @Override 369 public CaptureSession createSession() { 370 return new CaptureSessionImpl(null, System.currentTimeMillis(), null); 371 } 372 373 @Override 374 public void putSession(Uri sessionUri, CaptureSession session) { 375 synchronized (mSessions) { 376 mSessions.put(sessionUri.toString(), session); 377 } 378 } 379 380 @Override 381 public CaptureSession getSession(Uri sessionUri) { 382 synchronized (mSessions) { 383 return mSessions.get(sessionUri.toString()); 384 } 385 } 386 387 @Override 388 public void saveImage(byte[] data, String title, long date, Location loc, 389 int width, int height, int orientation, ExifInterface exif, 390 OnMediaSavedListener listener) { 391 mMediaSaver.addImage(data, title, date, loc, width, height, orientation, exif, 392 listener, mContentResolver); 393 } 394 395 @Override 396 public void addSessionListener(SessionListener listener) { 397 synchronized (mTaskListeners) { 398 mTaskListeners.add(listener); 399 } 400 } 401 402 @Override 403 public void removeSessionListener(SessionListener listener) { 404 synchronized (mTaskListeners) { 405 mTaskListeners.remove(listener); 406 } 407 } 408 409 @Override 410 public File getSessionDirectory(String subDirectory) throws IOException { 411 return mSessionStorageManager.getSessionDirectory(subDirectory); 412 } 413 414 private void removeSession(String sessionUri) { 415 synchronized (mSessions) { 416 mSessions.remove(sessionUri); 417 } 418 } 419 420 /** 421 * Notifies all task listeners that the task with the given URI has been 422 * queued. 423 */ 424 private void notifyTaskQueued(final Uri uri) { 425 mMainHandler.post(new Runnable() { 426 @Override 427 public void run() { 428 synchronized (mTaskListeners) { 429 for (SessionListener listener : mTaskListeners) { 430 listener.onSessionQueued(uri); 431 } 432 } 433 } 434 }); 435 } 436 437 /** 438 * Notifies all task listeners that the task with the given URI has been 439 * finished. 440 */ 441 private void notifyTaskDone(final Uri uri) { 442 mMainHandler.post(new Runnable() { 443 @Override 444 public void run() { 445 synchronized (mTaskListeners) { 446 for (SessionListener listener : mTaskListeners) { 447 listener.onSessionDone(uri); 448 } 449 } 450 } 451 }); 452 } 453 454 /** 455 * Notifies all task listeners that the task with the given URI has been 456 * failed to process. 457 */ 458 private void notifyTaskFailed(final Uri uri, final CharSequence reason) { 459 mMainHandler.post(new Runnable() { 460 @Override 461 public void run() { 462 synchronized (mTaskListeners) { 463 for (SessionListener listener : mTaskListeners) { 464 listener.onSessionFailed(uri, reason); 465 } 466 } 467 } 468 }); 469 } 470 471 /** 472 * Notifies all task listeners that the task with the given URI has 473 * progressed to the given state. 474 */ 475 private void notifyTaskProgress(final Uri uri, final int progressPercent) { 476 mMainHandler.post(new Runnable() { 477 @Override 478 public void run() { 479 synchronized (mTaskListeners) { 480 for (SessionListener listener : mTaskListeners) { 481 listener.onSessionProgress(uri, progressPercent); 482 } 483 } 484 } 485 }); 486 } 487 488 /** 489 * Notifies all task listeners that the task with the given URI has 490 * changed its progress message. 491 */ 492 private void notifyTaskProgressText(final Uri uri, final CharSequence message) { 493 mMainHandler.post(new Runnable() { 494 @Override 495 public void run() { 496 synchronized (mTaskListeners) { 497 for (SessionListener listener : mTaskListeners) { 498 listener.onSessionProgressText(uri, message); 499 } 500 } 501 } 502 }); 503 } 504 505 /** 506 * Notifies all task listeners that the task with the given URI has updated 507 * its media. 508 */ 509 private void notifySessionPreviewAvailable(final Uri uri) { 510 mMainHandler.post(new Runnable() { 511 @Override 512 public void run() { 513 synchronized (mTaskListeners) { 514 for (SessionListener listener : mTaskListeners) { 515 listener.onSessionPreviewAvailable(uri); 516 } 517 } 518 } 519 }); 520 } 521 522 @Override 523 public boolean hasErrorMessage(Uri uri) { 524 return mFailedSessionMessages.containsKey(uri); 525 } 526 527 @Override 528 public CharSequence getErrorMesage(Uri uri) { 529 return mFailedSessionMessages.get(uri); 530 } 531 532 @Override 533 public void removeErrorMessage(Uri uri) { 534 mFailedSessionMessages.remove(uri); 535 } 536 537 @Override 538 public void fillTemporarySession(final SessionListener listener) { 539 mMainHandler.post(new Runnable() { 540 @Override 541 public void run() { 542 synchronized (mSessions) { 543 for (String sessionUri : mSessions.keySet()) { 544 CaptureSession session = mSessions.get(sessionUri); 545 listener.onSessionQueued(session.getUri()); 546 listener.onSessionProgress(session.getUri(), session.getProgress()); 547 listener.onSessionProgressText(session.getUri(), 548 session.getProgressMessage()); 549 } 550 } 551 } 552 }); 553 } 554 } 555