1/*
2 * Copyright (C) 2010 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.videoeditor.service;
18
19import java.io.File;
20import java.io.FileNotFoundException;
21import java.io.FileOutputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.util.ArrayList;
25import java.util.Collections;
26import java.util.Comparator;
27import java.util.HashMap;
28import java.util.Iterator;
29import java.util.List;
30import java.util.Map;
31import java.util.concurrent.BlockingQueue;
32import java.util.concurrent.LinkedBlockingQueue;
33
34import android.app.Service;
35import android.content.ContentValues;
36import android.content.Context;
37import android.content.Intent;
38import android.database.Cursor;
39import android.graphics.Bitmap;
40import android.graphics.Rect;
41import android.media.videoeditor.AudioTrack;
42import android.media.videoeditor.Effect;
43import android.media.videoeditor.EffectColor;
44import android.media.videoeditor.EffectKenBurns;
45import android.media.videoeditor.ExtractAudioWaveformProgressListener;
46import android.media.videoeditor.MediaImageItem;
47import android.media.videoeditor.MediaItem;
48import android.media.videoeditor.MediaProperties;
49import android.media.videoeditor.MediaVideoItem;
50import android.media.videoeditor.Overlay;
51import android.media.videoeditor.OverlayFrame;
52import android.media.videoeditor.Transition;
53import android.media.videoeditor.TransitionAlpha;
54import android.media.videoeditor.TransitionCrossfade;
55import android.media.videoeditor.TransitionFadeBlack;
56import android.media.videoeditor.TransitionSliding;
57import android.media.videoeditor.VideoEditor;
58import android.media.videoeditor.VideoEditorFactory;
59import android.media.videoeditor.WaveformData;
60import android.media.videoeditor.MediaItem.GetThumbnailListCallback;
61import android.media.videoeditor.VideoEditor.ExportProgressListener;
62import android.media.videoeditor.VideoEditor.MediaProcessingProgressListener;
63import android.net.Uri;
64import android.os.Bundle;
65import android.os.Handler;
66import android.os.IBinder;
67import android.os.Looper;
68import android.provider.MediaStore;
69import android.provider.MediaStore.Audio;
70import android.provider.MediaStore.Images;
71import android.provider.MediaStore.Video;
72import android.util.Log;
73
74import com.android.videoeditor.R;
75import com.android.videoeditor.util.FileUtils;
76import com.android.videoeditor.util.ImageUtils;
77import com.android.videoeditor.util.MediaItemUtils;
78import com.android.videoeditor.util.StringUtils;
79
80/**
81 * VideoEditor service API
82 */
83public class ApiService extends Service {
84    // Logging
85    private static final String TAG = "VEApiService";
86
87    // Additional updates
88    public static final int ACTION_UPDATE_FRAME = MediaProcessingProgressListener.ACTION_DECODE + 100;
89    public static final int ACTION_NO_FRAME_UPDATE = MediaProcessingProgressListener.ACTION_DECODE + 101;
90
91    // Parameters
92    private static final String PARAM_OP = "op";
93    private static final String PARAM_REQUEST_ID = "rid";
94    private static final String PARAM_PROJECT_PATH = "project";
95    private static final String PARAM_FILENAME = "filename";
96    private static final String PARAM_STORYBOARD_ITEM_ID = "item_id";
97    private static final String PARAM_RELATIVE_STORYBOARD_ITEM_ID = "r_item_id";
98    private static final String PARAM_PROGRESS_VALUE = "prog_value";
99    private static final String PARAM_EXCEPTION = "ex";
100    private static final String PARAM_START_TIME = "s_time";
101    private static final String PARAM_END_TIME = "e_time";
102    private static final String PARAM_DURATION = "duration";
103    private static final String PARAM_WIDTH = "width";
104    private static final String PARAM_HEIGHT = "height";
105    private static final String PARAM_BITRATE = "bitrate";
106    private static final String PARAM_MEDIA_ITEM_RENDERING_MODE = "rm";
107    private static final String PARAM_MEDIA_ITEM_START_RECT = "start_rect";
108    private static final String PARAM_MEDIA_ITEM_END_RECT = "end_rect";
109    private static final String PARAM_EFFECT_TYPE = "e_type";
110    private static final String PARAM_EFFECT_PARAM = "e_param";
111    private static final String PARAM_TRANSITION_BEHAVIOR = "behavior";
112    private static final String PARAM_TRANSITION_MASK = "t_mask";
113    private static final String PARAM_TRANSITION_BLENDING = "t_blending";
114    private static final String PARAM_TRANSITION_INVERT = "t_invert";
115    private static final String PARAM_TRANSITION_DIRECTION = "t_dir";
116    private static final String PARAM_INTENT = "req_intent";
117    private static final String PARAM_PROJECT_NAME = "name";
118    private static final String PARAM_MOVIES_FILENAMES = "movies";
119    private static final String PARAM_PHOTOS_FILENAMES = "images";
120    private static final String PARAM_ASPECT_RATIO = "aspect_ratio";
121    private static final String PARAM_BEGIN_BOUNDARY = "b_boundary";
122    private static final String PARAM_END_BOUNDARY = "e_boundary";
123    private static final String PARAM_ATTRIBUTES = "attributes";
124    private static final String PARAM_VOLUME = "volume";
125    private static final String PARAM_LOOP = "loop";
126    private static final String PARAM_MUTE = "mute";
127    private static final String PARAM_DUCK = "duck";
128    private static final String PARAM_MOVIE_URI = "uri";
129    private static final String PARAM_THEME = "theme";
130    private static final String PARAM_ACTION = "action";
131    private static final String PARAM_COUNT = "count";
132    private static final String PARAM_TOKEN = "token";
133    private static final String PARAM_INDICES = "indices";
134    private static final String PARAM_CANCELLED = "cancelled";
135
136    // Operations
137    private static final int OP_VIDEO_EDITOR_CREATE = 1;
138    private static final int OP_VIDEO_EDITOR_LOAD = 2;
139    private static final int OP_VIDEO_EDITOR_SAVE = 3;
140    private static final int OP_VIDEO_EDITOR_EXPORT = 4;
141    private static final int OP_VIDEO_EDITOR_CANCEL_EXPORT = 5;
142    private static final int OP_VIDEO_EDITOR_EXPORT_STATUS = 6;
143    private static final int OP_VIDEO_EDITOR_RELEASE = 8;
144    private static final int OP_VIDEO_EDITOR_DELETE = 9;
145    private static final int OP_VIDEO_EDITOR_SET_ASPECT_RATIO = 10;
146    private static final int OP_VIDEO_EDITOR_APPLY_THEME = 11;
147    private static final int OP_VIDEO_EDITOR_GENERATE_PREVIEW_PROGRESS = 12;
148    private static final int OP_VIDEO_EDITOR_LOAD_PROJECTS = 13;
149
150    private static final int OP_MEDIA_ITEM_ADD_VIDEO_URI = 100;
151    private static final int OP_MEDIA_ITEM_ADD_IMAGE_URI = 101;
152    private static final int OP_MEDIA_ITEM_MOVE = 102;
153    private static final int OP_MEDIA_ITEM_REMOVE = 103;
154    private static final int OP_MEDIA_ITEM_SET_RENDERING_MODE = 104;
155    private static final int OP_MEDIA_ITEM_SET_DURATION = 105;
156    private static final int OP_MEDIA_ITEM_SET_BOUNDARIES = 106;
157    private static final int OP_MEDIA_ITEM_SET_VOLUME = 107;
158    private static final int OP_MEDIA_ITEM_SET_MUTE = 108;
159    private static final int OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM = 109;
160    private static final int OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM_STATUS = 110;
161    private static final int OP_MEDIA_ITEM_GET_THUMBNAILS = 112;
162    private static final int OP_MEDIA_ITEM_LOAD = 113;
163    private static final int OP_MEDIA_ITEM_LOAD_STATUS = 114;
164
165    private static final int OP_EFFECT_ADD_COLOR = 200;
166    private static final int OP_EFFECT_ADD_IMAGE_KEN_BURNS = 201;
167    private static final int OP_EFFECT_REMOVE = 202;
168
169    private static final int OP_TRANSITION_INSERT_ALPHA = 300;
170    private static final int OP_TRANSITION_INSERT_CROSSFADE = 301;
171    private static final int OP_TRANSITION_INSERT_FADE_BLACK = 302;
172    private static final int OP_TRANSITION_INSERT_SLIDING = 303;
173    private static final int OP_TRANSITION_REMOVE = 304;
174    private static final int OP_TRANSITION_SET_DURATION = 305;
175    private static final int OP_TRANSITION_GET_THUMBNAIL = 306;
176
177    private static final int OP_OVERLAY_ADD = 400;
178    private static final int OP_OVERLAY_REMOVE = 401;
179    private static final int OP_OVERLAY_SET_START_TIME = 402;
180    private static final int OP_OVERLAY_SET_DURATION = 403;
181    private static final int OP_OVERLAY_SET_ATTRIBUTES = 404;
182
183    private static final int OP_AUDIO_TRACK_ADD = 500;
184    private static final int OP_AUDIO_TRACK_REMOVE = 501;
185    private static final int OP_AUDIO_TRACK_SET_VOLUME = 502;
186    private static final int OP_AUDIO_TRACK_SET_MUTE = 503;
187    private static final int OP_AUDIO_TRACK_SET_BOUNDARIES = 505;
188    private static final int OP_AUDIO_TRACK_SET_LOOP = 506;
189    private static final int OP_AUDIO_TRACK_SET_DUCK = 507;
190    private static final int OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM = 508;
191    private static final int OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM_STATUS = 509;
192
193    private static final int DUCK_THRESHOLD = 20;
194    private static final int DUCK_TRACK_VOLUME = 65;
195    // The default audio track volume
196    private static final int DEFAULT_AUDIO_TRACK_VOLUME = 50;
197
198    // Static member variables
199    private static final Map<String, Intent> mPendingIntents = new HashMap<String, Intent>();
200    private static final List<ApiServiceListener> mListeners = new ArrayList<ApiServiceListener>();
201    private static final IntentPool mIntentPool = new IntentPool(8);
202    private static VideoEditorProject mVideoProject;
203    private static VideoEditor mVideoEditor;
204    private static ServiceMediaProcessingProgressListener mGeneratePreviewListener;
205    private static volatile boolean mExportCancelled;
206
207    private IntentProcessor mVideoThread;
208    private IntentProcessor mAudioThread;
209    private IntentProcessor mThumbnailThread;
210    private Handler mHandler;
211
212    private final Runnable mStopRunnable = new Runnable() {
213        @Override
214        public void run() {
215            if (mPendingIntents.size() == 0) {
216                logd("Stop runnable: Stopping service");
217                stopSelf();
218            }
219        }
220    };
221
222    /**
223     * Generate preview listener
224     */
225    private final class ServiceMediaProcessingProgressListener
226            implements VideoEditor.MediaProcessingProgressListener {
227        // Instance variables
228        private final String mProjectPath;
229
230        /**
231         * Constructor
232         *
233         * @param projectPath The project path
234         */
235        public ServiceMediaProcessingProgressListener(String projectPath) {
236            mProjectPath = projectPath;
237        }
238
239        @Override
240        public void onProgress(Object item, int action, int progress) {
241            final Intent intent = mIntentPool.get();
242            intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_GENERATE_PREVIEW_PROGRESS);
243            intent.putExtra(PARAM_PROJECT_PATH, mProjectPath);
244            intent.putExtra(PARAM_ACTION, action);
245            intent.putExtra(PARAM_PROGRESS_VALUE, progress);
246
247            if (item == null) { // Last callback uses null
248            } else if (item instanceof MediaItem) {
249                intent.putExtra(PARAM_STORYBOARD_ITEM_ID, ((MediaItem)item).getId());
250                intent.putExtra(PARAM_ATTRIBUTES, MediaItem.class.getCanonicalName());
251            } else if (item instanceof Transition) {
252                intent.putExtra(PARAM_STORYBOARD_ITEM_ID, ((Transition)item).getId());
253                intent.putExtra(PARAM_ATTRIBUTES, Transition.class.getCanonicalName());
254            } else if (item instanceof AudioTrack) {
255                intent.putExtra(PARAM_STORYBOARD_ITEM_ID, ((AudioTrack)item).getId());
256                intent.putExtra(PARAM_ATTRIBUTES, AudioTrack.class.getCanonicalName());
257            } else {
258                Log.w(TAG, "Unsupported storyboard item type: " + item.getClass());
259                return;
260            }
261
262            completeRequest(intent, null, null, null, null, true);
263        }
264    }
265
266    /**
267     * @return A unique id
268     */
269    public static String generateId() {
270        return StringUtils.randomString(6);
271    }
272
273    /**
274     * Register a listener
275     *
276     * @param listener The listener
277     */
278    public static void registerListener(ApiServiceListener listener) {
279        mListeners.add(listener);
280    }
281
282    /**
283     * Unregister a listener
284     *
285     * @param listener The listener
286     */
287    public static void unregisterListener(ApiServiceListener listener) {
288        mListeners.remove(listener);
289    }
290
291    /**
292     * Load the projects
293     *
294     * @param context The context
295     */
296    public static void loadProjects(Context context) {
297        final Intent intent = mIntentPool.get(context, ApiService.class);
298        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_LOAD_PROJECTS);
299
300        startCommand(context, intent);
301    }
302
303    /**
304     * Create a new VideoEditor project
305     *
306     * @param context The context
307     * @param projectPath The project path
308     * @param projectName The project name
309     * @param movies The array of movie file names to add to the newly
310     *      created project
311     * @param photos The array of photo file names to add to the newly
312     *      created project
313     * @param themeType The theme type
314     */
315    public static void createVideoEditor(Context context, String projectPath, String projectName,
316                String[] movies, String[] photos, String themeType) {
317        final Intent intent = mIntentPool.get(context, ApiService.class);
318        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_CREATE);
319        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
320        intent.putExtra(PARAM_PROJECT_NAME, projectName);
321        intent.putExtra(PARAM_MOVIES_FILENAMES, movies);
322        intent.putExtra(PARAM_PHOTOS_FILENAMES, photos);
323        intent.putExtra(PARAM_THEME, themeType);
324
325        startCommand(context, intent);
326    }
327
328    /**
329     * Create a new VideoEditor project
330     *
331     * @param context The context
332     * @param projectPath The project path
333     */
334    public static void loadVideoEditor(Context context, String projectPath) {
335        final Intent intent = mIntentPool.get(context, ApiService.class);
336        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_LOAD);
337        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
338
339        startCommand(context, intent);
340    }
341
342    /**
343     * Export the VideoEditor movie
344     *
345     * @param context The context
346     * @param projectPath The project path
347     * @param filename The export filename
348     * @param height The output movie height
349     * @param bitrate The output movie bitrate
350     */
351    public static void exportVideoEditor(Context context, String projectPath, String filename,
352            int height, int bitrate) {
353        final Intent intent = mIntentPool.get(context, ApiService.class);
354        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_EXPORT);
355        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
356        intent.putExtra(PARAM_FILENAME, filename);
357        intent.putExtra(PARAM_HEIGHT, height);
358        intent.putExtra(PARAM_BITRATE, bitrate);
359
360        startCommand(context, intent);
361    }
362
363    /**
364     * Check if export is pending
365     *
366     * @param projectPath The project path
367     * @param filename The export filename
368     *
369     * @return true if the export is pending
370     */
371    public static boolean isVideoEditorExportPending(String projectPath, String filename) {
372        for (Intent intent : mPendingIntents.values()) {
373            final int op = intent.getIntExtra(PARAM_OP, -1);
374            if (op == OP_VIDEO_EDITOR_EXPORT) {
375                String pp = intent.getStringExtra(PARAM_PROJECT_PATH);
376                if (pp.equals(projectPath)) {
377                    String fn = intent.getStringExtra(PARAM_FILENAME);
378                    if (fn.equals(filename)) {
379                        return true;
380                    }
381                }
382            }
383        }
384
385        return false;
386    }
387
388    /**
389     * Cancel the export of the specified VideoEditor movie
390     *
391     * @param context The context
392     * @param projectPath The project path
393     * @param filename The export filename
394     */
395    public static void cancelExportVideoEditor(Context context, String projectPath,
396            String filename) {
397        mExportCancelled = true;
398        final Intent intent = mIntentPool.get(context, ApiService.class);
399        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_CANCEL_EXPORT);
400        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
401        intent.putExtra(PARAM_FILENAME, filename);
402
403        startCommand(context, intent);
404    }
405
406    /**
407     * Change the aspect ratio
408     *
409     * @param context The context
410     * @param projectPath The project path
411     * @param aspectRatio The aspect ratio
412     */
413    public static void setAspectRatio(Context context, String projectPath, int aspectRatio) {
414        final Intent intent = mIntentPool.get(context, ApiService.class);
415        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_SET_ASPECT_RATIO);
416        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
417        intent.putExtra(PARAM_ASPECT_RATIO, aspectRatio);
418
419        startCommand(context, intent);
420    }
421
422    /**
423     * Apply a theme
424     *
425     * @param context The context
426     * @param projectPath The project path
427     * @param theme The theme
428     */
429     public static void applyTheme(Context context, String projectPath, String theme) {
430         final Intent intent = mIntentPool.get(context, ApiService.class);
431         intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_APPLY_THEME);
432         intent.putExtra(PARAM_PROJECT_PATH, projectPath);
433         intent.putExtra(PARAM_THEME, theme);
434
435         startCommand(context, intent);
436     }
437
438    /**
439     * Checks if the service is busy modifying the timeline. While
440     * the video editor is busy the application should not attempt
441     * to preview the movie.
442     *
443     * @param projectPath The project path
444     *
445     * @return {@code true} if the video editor is modifying the timeline
446     */
447    public static boolean isProjectBeingEdited(String projectPath) {
448        for (Intent intent : mPendingIntents.values()) {
449            final int op = intent.getIntExtra(PARAM_OP, -1);
450            switch (op) {
451                // When these operations are pending the video editor is not busy.
452                case OP_VIDEO_EDITOR_LOAD_PROJECTS:
453                case OP_VIDEO_EDITOR_SAVE:
454                case OP_MEDIA_ITEM_SET_VOLUME:
455                case OP_MEDIA_ITEM_SET_MUTE:
456                case OP_MEDIA_ITEM_GET_THUMBNAILS:
457                case OP_MEDIA_ITEM_LOAD:
458                case OP_TRANSITION_GET_THUMBNAIL:
459                case OP_AUDIO_TRACK_SET_VOLUME:
460                case OP_AUDIO_TRACK_SET_MUTE: {
461                    break;
462                }
463
464                default: {
465                    final String pp = intent.getStringExtra(PARAM_PROJECT_PATH);
466                    if (pp != null && pp.equals(projectPath)) {
467                        return true;
468                    }
469                    break;
470                }
471            }
472        }
473
474        return false;
475    }
476
477    /**
478     * Save the VideoEditor project
479     *
480     * @param context The context
481     * @param projectPath The project path
482     */
483    public static void saveVideoEditor(Context context, String projectPath) {
484        final Intent intent = mIntentPool.get(context, ApiService.class);
485        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_SAVE);
486        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
487
488        startCommand(context, intent);
489    }
490
491    /**
492     * Release the VideoEditor project
493     *
494     * @param context The context
495     * @param projectPath The project path
496     */
497    public static void releaseVideoEditor(Context context, String projectPath) {
498        final Intent intent = mIntentPool.get(context, ApiService.class);
499        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_RELEASE);
500        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
501
502        startCommand(context, intent);
503    }
504
505    /**
506     * Delete the project specified by the project path
507     *
508     * @param context The context
509     * @param projectPath The project path
510     */
511    public static void deleteProject(Context context, String projectPath) {
512        final Intent intent = mIntentPool.get(context, ApiService.class);
513        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_DELETE);
514        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
515
516        startCommand(context, intent);
517    }
518
519    /**
520     * Add a new video media item after the specified media item id
521     *
522     * @param context The context
523     * @param projectPath The project path
524     * @param mediaItemId The mediaItem id
525     * @param afterMediaItemId The id of the media item preceding the media item
526     * @param uri The media item URI
527     * @param renderingMode The rendering mode
528     * @param themeId The theme id
529     */
530    public static void addMediaItemVideoUri(Context context, String projectPath,
531            String mediaItemId, String afterMediaItemId, Uri uri, int renderingMode,
532            String themeId) {
533        final Intent intent = mIntentPool.get(context, ApiService.class);
534        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_ADD_VIDEO_URI);
535        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
536        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
537        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, afterMediaItemId);
538        intent.putExtra(PARAM_FILENAME, uri);
539        intent.putExtra(PARAM_MEDIA_ITEM_RENDERING_MODE, renderingMode);
540        intent.putExtra(PARAM_THEME, themeId);
541
542        startCommand(context, intent);
543    }
544
545    /**
546     * Add a new image media item after the specified media item id
547     *
548     * @param context The context
549     * @param projectPath The project path
550     * @param mediaItemId The mediaItem id
551     * @param afterMediaItemId The id of the media item preceding the media item
552     * @param uri The media item URI
553     * @param renderingMode The rendering mode
554     * @param durationMs The duration of the item (for images only, ignored for videos)
555     * @param themeId The theme id
556     */
557    public static void addMediaItemImageUri(Context context, String projectPath,
558            String mediaItemId, String afterMediaItemId, Uri uri, int renderingMode,
559            long durationMs, String themeId) {
560        final Intent intent = mIntentPool.get(context, ApiService.class);
561        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_ADD_IMAGE_URI);
562        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
563        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
564        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, afterMediaItemId);
565        intent.putExtra(PARAM_FILENAME, uri);
566        intent.putExtra(PARAM_MEDIA_ITEM_RENDERING_MODE, renderingMode);
567        intent.putExtra(PARAM_DURATION, durationMs);
568        intent.putExtra(PARAM_THEME, themeId);
569
570        startCommand(context, intent);
571    }
572
573    /**
574     * Download or make a copy of an image from the specified URI
575     *
576     * @param context The context
577     * @param projectPath The project path
578     * @param uri The media item URI
579     * @param mimeType The MIME type
580     */
581    public static void loadMediaItem(Context context, String projectPath, Uri uri,
582            String mimeType) {
583        final Intent intent = mIntentPool.get(context, ApiService.class);
584        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_LOAD);
585        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
586        intent.putExtra(PARAM_FILENAME, uri);
587        intent.putExtra(PARAM_ATTRIBUTES, mimeType);
588
589        startCommand(context, intent);
590    }
591
592    /**
593     * Move a media item after the specified media id
594     *
595     * @param context The context
596     * @param projectPath The project path
597     * @param mediaItemId The id of the media item to move
598     * @param afterMediaItemId The id of the relative media item
599     * @param themeId The theme id
600     */
601    public static void moveMediaItem(Context context, String projectPath,
602            String mediaItemId, String afterMediaItemId, String themeId) {
603        final Intent intent = mIntentPool.get(context, ApiService.class);
604        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_MOVE);
605        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
606        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
607        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, afterMediaItemId);
608        intent.putExtra(PARAM_THEME, themeId);
609
610        startCommand(context, intent);
611    }
612
613    /**
614     * Remove a media item
615     *
616     * @param context The context
617     * @param projectPath The project path
618     * @param mediaItemId The id of the media item to remove
619     * @param themeId The theme id
620     */
621    public static void removeMediaItem(Context context, String projectPath, String mediaItemId,
622            String themeId) {
623        final Intent intent = mIntentPool.get(context, ApiService.class);
624        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_REMOVE);
625        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
626        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
627        intent.putExtra(PARAM_THEME, themeId);
628
629        startCommand(context, intent);
630    }
631
632    /**
633     * Set the rendering mode for a media item
634     *
635     * @param context The context
636     * @param projectPath The project path
637     * @param mediaItemId The id of the media item
638     */
639    public static void setMediaItemRenderingMode(Context context, String projectPath,
640            String mediaItemId, int renderingMode) {
641        final Intent intent = mIntentPool.get(context, ApiService.class);
642        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_SET_RENDERING_MODE);
643        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
644        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
645        intent.putExtra(PARAM_MEDIA_ITEM_RENDERING_MODE, renderingMode);
646
647        startCommand(context, intent);
648    }
649
650    /**
651     * Get the thumbnails of the specified size
652     *
653     * @param context The context
654     * @param projectPath The project path
655     * @param mediaItemId The id of the media item
656     * @param width The width
657     * @param height The height
658     * @param startMs The start time in milliseconds
659     * @param endMs The end time in milliseconds
660     * @param count The number of thumbnails
661     */
662    public static void getMediaItemThumbnails(Context context,
663            String projectPath, String mediaItemId, int width, int height,
664            long startMs, long endMs, int count, int token, int[] indices) {
665        final Intent intent = mIntentPool.get(context, ApiService.class);
666        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_GET_THUMBNAILS);
667        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
668        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
669        intent.putExtra(PARAM_WIDTH, width);
670        intent.putExtra(PARAM_HEIGHT, height);
671        intent.putExtra(PARAM_START_TIME, startMs);
672        intent.putExtra(PARAM_END_TIME, endMs);
673        intent.putExtra(PARAM_COUNT, count);
674        intent.putExtra(PARAM_TOKEN, token);
675        intent.putExtra(PARAM_INDICES, indices);
676
677        startCommand(context, intent);
678    }
679
680    /**
681     * Set the media item duration
682     *
683     * @param context The context
684     * @param projectPath The project path
685     * @param mediaItemId The id of the media item
686     * @param durationMs The media item duration
687     */
688    public static void setMediaItemDuration(Context context, String projectPath,
689            String mediaItemId, long durationMs) {
690        final Intent intent = mIntentPool.get(context, ApiService.class);
691        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_SET_DURATION);
692        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
693        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
694        intent.putExtra(PARAM_DURATION, durationMs);
695
696        startCommand(context, intent);
697    }
698
699    /**
700     * Set the media item boundaries
701     *
702     * @param context The context
703     * @param projectPath The project path
704     * @param mediaItemId The id of the media item
705     * @param beginBoundaryMs The media item begin boundary
706     * @param endBoundaryMs The media item end boundary
707     */
708    public static void setMediaItemBoundaries(Context context, String projectPath,
709            String mediaItemId, long beginBoundaryMs, long endBoundaryMs) {
710        final Intent intent = mIntentPool.get(context, ApiService.class);
711        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_SET_BOUNDARIES);
712        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
713        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
714        intent.putExtra(PARAM_BEGIN_BOUNDARY, beginBoundaryMs);
715        intent.putExtra(PARAM_END_BOUNDARY, endBoundaryMs);
716
717        startCommand(context, intent);
718    }
719
720    /**
721     * Set the media item volume (MediaVideoItem only)
722     *
723     * @param context The context
724     * @param projectPath The project path
725     * @param mediaItemId The id of the media item
726     * @param volumePercentage The volume
727     */
728    public static void setMediaItemVolume(Context context, String projectPath,
729            String mediaItemId, int volumePercentage) {
730        final Intent intent = mIntentPool.get(context, ApiService.class);
731        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_SET_VOLUME);
732        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
733        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
734        intent.putExtra(PARAM_VOLUME, volumePercentage);
735
736        startCommand(context, intent);
737    }
738
739    /**
740     * Mute/unmute the media item (MediaVideoItem only)
741     *
742     * @param context The context
743     * @param projectPath The project path
744     * @param mediaItemId The id of the media item
745     * @param muted true to mute the media item
746     */
747    public static void setMediaItemMute(Context context, String projectPath, String mediaItemId,
748            boolean muted) {
749        final Intent intent = mIntentPool.get(context, ApiService.class);
750        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_SET_MUTE);
751        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
752        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
753        intent.putExtra(PARAM_MUTE, muted);
754
755        startCommand(context, intent);
756    }
757
758    /**
759     * Extract the media item audio waveform
760     *
761     * @param context The context
762     * @param projectPath The project path
763     * @param mediaItemId The id of the media item
764     */
765    public static void extractMediaItemAudioWaveform(Context context, String projectPath,
766            String mediaItemId) {
767        if (isMediaItemAudioWaveformPending(projectPath, mediaItemId)) {
768            return;
769        }
770
771        final Intent intent = mIntentPool.get(context, ApiService.class);
772        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM);
773        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
774        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
775
776        startCommand(context, intent);
777    }
778
779    /**
780     * Check if extract audio waveform is pending for the specified MediaItem
781     *
782     * @param projectPath The project path
783     * @param mediaItemId The MediaItem id
784     *
785     * @return true if the extract audio waveform is pending
786     */
787    public static boolean isMediaItemAudioWaveformPending(String projectPath, String mediaItemId) {
788        for (Intent intent : mPendingIntents.values()) {
789            int op = intent.getIntExtra(PARAM_OP, -1);
790            if (op == OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM) {
791                String pp = intent.getStringExtra(PARAM_PROJECT_PATH);
792                if (pp.equals(projectPath)) {
793                    String mid = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
794                    if (mid.equals(mediaItemId)) {
795                        return true;
796                    }
797                }
798            }
799        }
800
801        return false;
802    }
803
804    /**
805     * Insert an alpha transition after the specified media item
806     *
807     * @param context The context
808     * @param projectPath The project path
809     * @param afterMediaItemId Insert the transition after the media item with this id
810     * @param transitionId The transition id
811     * @param durationMs The duration in milliseconds
812     * @param behavior The transition behavior
813     * @param maskRawResourceId The mask raw resource id
814     * @param blending The transition blending
815     * @param invert The transition invert
816     */
817    public static void insertAlphaTransition(Context context, String projectPath,
818            String afterMediaItemId, String transitionId, long durationMs, int behavior,
819            int maskRawResourceId, int blending, boolean invert) {
820        final Intent intent = mIntentPool.get(context, ApiService.class);
821        intent.putExtra(PARAM_OP, OP_TRANSITION_INSERT_ALPHA);
822        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
823        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, afterMediaItemId);
824        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, transitionId);
825        intent.putExtra(PARAM_DURATION, durationMs);
826        intent.putExtra(PARAM_TRANSITION_BEHAVIOR, behavior);
827        intent.putExtra(PARAM_TRANSITION_MASK, maskRawResourceId);
828        intent.putExtra(PARAM_TRANSITION_BLENDING, blending);
829        intent.putExtra(PARAM_TRANSITION_INVERT, invert);
830
831        startCommand(context, intent);
832    }
833
834    /**
835     * Insert an crossfade transition after the specified media item
836     *
837     * @param context The context
838     * @param projectPath The project path
839     * @param afterMediaItemId Insert the transition after the media item with this id
840     * @param transitionId The transition id
841     * @param durationMs The duration in milliseconds
842     * @param behavior The transition behavior
843     */
844    public static void insertCrossfadeTransition(Context context, String projectPath,
845            String afterMediaItemId, String transitionId, long durationMs, int behavior) {
846        final Intent intent = mIntentPool.get(context, ApiService.class);
847        intent.putExtra(PARAM_OP, OP_TRANSITION_INSERT_CROSSFADE);
848        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
849        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, afterMediaItemId);
850        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, transitionId);
851        intent.putExtra(PARAM_DURATION, durationMs);
852        intent.putExtra(PARAM_TRANSITION_BEHAVIOR, behavior);
853
854        startCommand(context, intent);
855    }
856
857    /**
858     * Insert a fade-to-black transition after the specified media item
859     *
860     * @param context The context
861     * @param projectPath The project path
862     * @param afterMediaItemId Insert the transition after the media item with this id
863     * @param transitionId The transition id
864     * @param durationMs The duration in milliseconds
865     * @param behavior The transition behavior
866     */
867    public static void insertFadeBlackTransition(Context context, String projectPath,
868            String afterMediaItemId, String transitionId, long durationMs, int behavior) {
869        final Intent intent = mIntentPool.get(context, ApiService.class);
870        intent.putExtra(PARAM_OP, OP_TRANSITION_INSERT_FADE_BLACK);
871        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
872        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, afterMediaItemId);
873        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, transitionId);
874        intent.putExtra(PARAM_DURATION, durationMs);
875        intent.putExtra(PARAM_TRANSITION_BEHAVIOR, behavior);
876
877        startCommand(context, intent);
878    }
879
880    /**
881     * Insert a sliding transition after the specified media item
882     *
883     * @param context The context
884     * @param projectPath The project path
885     * @param afterMediaItemId Insert the transition after the media item with this id
886     * @param transitionId The transition id
887     * @param durationMs The duration in milliseconds
888     * @param behavior The transition behavior
889     * @param direction The slide direction
890     */
891    public static void insertSlidingTransition(Context context, String projectPath,
892            String afterMediaItemId, String transitionId, long durationMs, int behavior,
893            int direction) {
894        final Intent intent = mIntentPool.get(context, ApiService.class);
895        intent.putExtra(PARAM_OP, OP_TRANSITION_INSERT_SLIDING);
896        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
897        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, afterMediaItemId);
898        intent.putExtra(PARAM_DURATION, durationMs);
899        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, transitionId);
900        intent.putExtra(PARAM_TRANSITION_BEHAVIOR, behavior);
901        intent.putExtra(PARAM_TRANSITION_DIRECTION, direction);
902
903        startCommand(context, intent);
904    }
905
906    /**
907     * Remove a transition
908     *
909     * @param context The context
910     * @param projectPath The project path
911     * @param transitionId The id of the transition to remove
912     */
913    public static void removeTransition(Context context, String projectPath, String transitionId) {
914        final Intent intent = mIntentPool.get(context, ApiService.class);
915        intent.putExtra(PARAM_OP, OP_TRANSITION_REMOVE);
916        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
917        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, transitionId);
918
919        startCommand(context, intent);
920    }
921
922    /**
923     * Set a transition duration
924     *
925     * @param context The context
926     * @param projectPath The project path
927     * @param transitionId The id of the transition
928     * @param durationMs The transition duration
929     */
930    public static void setTransitionDuration(Context context, String projectPath,
931            String transitionId, long durationMs) {
932        final Intent intent = mIntentPool.get(context, ApiService.class);
933        intent.putExtra(PARAM_OP, OP_TRANSITION_SET_DURATION);
934        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
935        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, transitionId);
936        intent.putExtra(PARAM_DURATION, durationMs);
937
938        startCommand(context, intent);
939    }
940
941    /**
942     * Get the thumbnail of the specified height
943     *
944     * @param context The context
945     * @param projectPath The project path
946     * @param transitionId The id of the transition
947     * @param height The height
948     */
949    public static void getTransitionThumbnails(Context context, String projectPath,
950            String transitionId, int height) {
951        final Intent intent = mIntentPool.get(context, ApiService.class);
952        intent.putExtra(PARAM_OP, OP_TRANSITION_GET_THUMBNAIL);
953        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
954        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, transitionId);
955        intent.putExtra(PARAM_HEIGHT, height);
956
957        startCommand(context, intent);
958    }
959
960    /**
961     * Check if the transition thumbnailing is in progress
962     *
963     * @param projectPath The project path
964     * @param transitionId The id of the transition
965     *
966     * @return true if the transition thumbnailing is in progress
967     */
968    public static boolean isTransitionThumbnailsPending(String projectPath, String transitionId) {
969        for (Intent intent : mPendingIntents.values()) {
970            int op = intent.getIntExtra(PARAM_OP, -1);
971            if (op == OP_TRANSITION_GET_THUMBNAIL) {
972                String pp = intent.getStringExtra(PARAM_PROJECT_PATH);
973                if (pp.equals(projectPath)) {
974                    String mid = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
975                    if (mid.equals(transitionId)) {
976                        return true;
977                    }
978                }
979            }
980        }
981
982        return false;
983    }
984
985    /**
986     * Add a color effect
987     *
988     * @param context The context
989     * @param projectPath The project path
990     * @param mediaItemId The media item id
991     * @param effectId The effect id
992     * @param startTimeMs The start time in milliseconds
993     * @param durationMs The duration in milliseconds
994     * @param type The effect type
995     * @param param The effect param (if any)
996     */
997    public static void addEffectColor(Context context, String projectPath, String mediaItemId,
998            String effectId, long startTimeMs, long durationMs, int type, int param) {
999        final Intent intent = mIntentPool.get(context, ApiService.class);
1000        intent.putExtra(PARAM_OP, OP_EFFECT_ADD_COLOR);
1001        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1002        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, mediaItemId);
1003        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, effectId);
1004        intent.putExtra(PARAM_START_TIME, startTimeMs);
1005        intent.putExtra(PARAM_DURATION, durationMs);
1006        intent.putExtra(PARAM_EFFECT_TYPE, type);
1007        intent.putExtra(PARAM_EFFECT_PARAM, param);
1008
1009        startCommand(context, intent);
1010    }
1011
1012    /**
1013     * Add a Ken Burns effect
1014     *
1015     * @param context The context
1016     * @param projectPath The project path
1017     * @param mediaItemId The media item id
1018     * @param effectId The effect id
1019     * @param startTimeMs The start time
1020     * @param durationMs The duration of the item
1021     * @param startRect The start rectangle for the Ken Burns effect
1022     * @param endRect The end rectangle for the Ken Burns effect
1023     */
1024    public static void addEffectKenBurns(Context context, String projectPath,
1025            String mediaItemId, String effectId, long startTimeMs, long durationMs,
1026            Rect startRect, Rect endRect) {
1027        final Intent intent = mIntentPool.get(context, ApiService.class);
1028        intent.putExtra(PARAM_OP, OP_EFFECT_ADD_IMAGE_KEN_BURNS);
1029        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1030        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, mediaItemId);
1031        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, effectId);
1032        intent.putExtra(PARAM_START_TIME, startTimeMs);
1033        intent.putExtra(PARAM_DURATION, durationMs);
1034        intent.putExtra(PARAM_MEDIA_ITEM_START_RECT, startRect);
1035        intent.putExtra(PARAM_MEDIA_ITEM_END_RECT, endRect);
1036
1037        startCommand(context, intent);
1038    }
1039
1040    /**
1041     * Remove an effect
1042     *
1043     * @param context The context
1044     * @param projectPath The project path
1045     * @param mediaItemId The media item id
1046     * @param effectId The id of the effect to remove
1047     */
1048    public static void removeEffect(Context context, String projectPath, String mediaItemId,
1049            String effectId) {
1050        final Intent intent = mIntentPool.get(context, ApiService.class);
1051        intent.putExtra(PARAM_OP, OP_EFFECT_REMOVE);
1052        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1053        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, mediaItemId);
1054        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, effectId);
1055
1056        startCommand(context, intent);
1057    }
1058
1059    /**
1060     * Add an overlay
1061     *
1062     * @param context The context
1063     * @param projectPath The project path
1064     * @param mediaItemId The media item id
1065     * @param userAttributes The overlay user attributes
1066     * @param startTimeMs The start time in milliseconds
1067     * @param durationMs The duration in milliseconds
1068     */
1069    public static void addOverlay(Context context, String projectPath, String mediaItemId,
1070            String overlayId, Bundle userAttributes, long startTimeMs, long durationMs) {
1071        final Intent intent = mIntentPool.get(context, ApiService.class);
1072        intent.putExtra(PARAM_OP, OP_OVERLAY_ADD);
1073        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1074        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, mediaItemId);
1075        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, overlayId);
1076        intent.putExtra(PARAM_START_TIME, startTimeMs);
1077        intent.putExtra(PARAM_DURATION, durationMs);
1078        intent.putExtra(PARAM_ATTRIBUTES, userAttributes);
1079
1080        startCommand(context, intent);
1081    }
1082
1083    /**
1084     * Remove an overlay
1085     *
1086     * @param context The context
1087     * @param projectPath The project path
1088     * @param mediaItemId The media item id
1089     * @param overlayId The id of the overlay to remove
1090     */
1091    public static void removeOverlay(Context context, String projectPath, String mediaItemId,
1092            String overlayId) {
1093        final Intent intent = mIntentPool.get(context, ApiService.class);
1094        intent.putExtra(PARAM_OP, OP_OVERLAY_REMOVE);
1095        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1096        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, mediaItemId);
1097        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, overlayId);
1098
1099        startCommand(context, intent);
1100    }
1101
1102    /**
1103     * Set the start time of an overlay
1104     *
1105     * @param context The context
1106     * @param projectPath The project path
1107     * @param mediaItemId The media item id
1108     * @param overlayId The id of the overlay
1109     * @param startTimeMs The start time in milliseconds
1110     */
1111    public static void setOverlayStartTime(Context context, String projectPath, String mediaItemId,
1112            String overlayId, long startTimeMs) {
1113        final Intent intent = mIntentPool.get(context, ApiService.class);
1114        intent.putExtra(PARAM_OP, OP_OVERLAY_SET_START_TIME);
1115        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1116        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, mediaItemId);
1117        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, overlayId);
1118        intent.putExtra(PARAM_START_TIME, startTimeMs);
1119
1120        startCommand(context, intent);
1121    }
1122
1123    /**
1124     * Set the duration of an overlay
1125     *
1126     * @param context The context
1127     * @param projectPath The project path
1128     * @param mediaItemId The media item id
1129     * @param overlayId The id of the overlay
1130     * @param durationMs The duration in milliseconds
1131     */
1132    public static void setOverlayDuration(Context context, String projectPath, String mediaItemId,
1133            String overlayId, long durationMs) {
1134        final Intent intent = mIntentPool.get(context, ApiService.class);
1135        intent.putExtra(PARAM_OP, OP_OVERLAY_SET_DURATION);
1136        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1137        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, mediaItemId);
1138        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, overlayId);
1139        intent.putExtra(PARAM_DURATION, durationMs);
1140
1141        startCommand(context, intent);
1142    }
1143
1144    /**
1145     * Set the user attributes of an overlay
1146     *
1147     * @param context The context
1148     * @param projectPath The project path
1149     * @param mediaItemId The media item id
1150     * @param overlayId The id of the overlay
1151     * @param userAttributes The user attributes
1152     */
1153    public static void setOverlayUserAttributes(Context context, String projectPath,
1154            String mediaItemId, String overlayId, Bundle userAttributes) {
1155        final Intent intent = mIntentPool.get(context, ApiService.class);
1156        intent.putExtra(PARAM_OP, OP_OVERLAY_SET_ATTRIBUTES);
1157        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1158        intent.putExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID, mediaItemId);
1159        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, overlayId);
1160        intent.putExtra(PARAM_ATTRIBUTES, userAttributes);
1161
1162        startCommand(context, intent);
1163    }
1164
1165    /**
1166     * Add an audio track
1167     *
1168     * @param context The context
1169     * @param projectPath The project path
1170     * @param id The audio track id
1171     * @param uri The audio track URI
1172     * @param loop true to loop the audio track
1173     */
1174    public static void addAudioTrack(Context context, String projectPath, String id, Uri uri,
1175            boolean loop) {
1176        final Intent intent = mIntentPool.get(context, ApiService.class);
1177        intent.putExtra(PARAM_OP, OP_AUDIO_TRACK_ADD);
1178        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1179        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, id);
1180        intent.putExtra(PARAM_FILENAME, uri);
1181        intent.putExtra(PARAM_LOOP, loop);
1182
1183        startCommand(context, intent);
1184    }
1185
1186    /**
1187     * Remove an audio track from the storyboard timeline
1188     *
1189     * @param context The context
1190     * @param projectPath The project path
1191     * @param audioTrackId The id of the audio track to remove
1192     */
1193    public static void removeAudioTrack(Context context, String projectPath, String audioTrackId) {
1194        final Intent intent = mIntentPool.get(context, ApiService.class);
1195        intent.putExtra(PARAM_OP, OP_AUDIO_TRACK_REMOVE);
1196        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1197        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, audioTrackId);
1198
1199        startCommand(context, intent);
1200    }
1201
1202    /**
1203     * Set the audio track boundaries
1204     *
1205     * @param context The context
1206     * @param projectPath The project path
1207     * @param audioTrackId The id of the audio track
1208     * @param beginBoundaryMs The audio track begin boundary
1209     * @param endBoundaryMs The audio track end boundary
1210     */
1211    public static void setAudioTrackBoundaries(Context context, String projectPath,
1212            String audioTrackId, long beginBoundaryMs, long endBoundaryMs) {
1213        final Intent intent = mIntentPool.get(context, ApiService.class);
1214        intent.putExtra(PARAM_OP, OP_AUDIO_TRACK_SET_BOUNDARIES);
1215        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1216        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, audioTrackId);
1217        intent.putExtra(PARAM_BEGIN_BOUNDARY, beginBoundaryMs);
1218        intent.putExtra(PARAM_END_BOUNDARY, endBoundaryMs);
1219
1220        startCommand(context, intent);
1221    }
1222
1223    /**
1224     * Set the loop flag for an audio track
1225     *
1226     * @param context The context
1227     * @param projectPath The project path
1228     * @param audioTrackId The id of the audio track
1229     * @param loop true to loop audio
1230     */
1231    public static void setAudioTrackLoop(Context context, String projectPath, String audioTrackId,
1232            boolean loop) {
1233        final Intent intent = mIntentPool.get(context, ApiService.class);
1234        intent.putExtra(PARAM_OP, OP_AUDIO_TRACK_SET_LOOP);
1235        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1236        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, audioTrackId);
1237        intent.putExtra(PARAM_LOOP, loop);
1238
1239        startCommand(context, intent);
1240    }
1241
1242    /**
1243     * Set the duck flag for an audio track
1244     *
1245     * @param context The context
1246     * @param projectPath The project path
1247     * @param audioTrackId The id of the audio track
1248     * @param duck true to enable ducking
1249     */
1250    public static void setAudioTrackDuck(Context context, String projectPath, String audioTrackId,
1251            boolean duck) {
1252        final Intent intent = mIntentPool.get(context, ApiService.class);
1253        intent.putExtra(PARAM_OP, OP_AUDIO_TRACK_SET_DUCK);
1254        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1255        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, audioTrackId);
1256        intent.putExtra(PARAM_DUCK, duck);
1257
1258        startCommand(context, intent);
1259    }
1260
1261    /**
1262     * Set the audio track volume
1263     *
1264     * @param context The context
1265     * @param projectPath The project path
1266     * @param audioTrackId The id of the audio track
1267     * @param volumePercentage The audio track volume (in percentage)
1268     */
1269    public static void setAudioTrackVolume(Context context, String projectPath,
1270            String audioTrackId, int volumePercentage) {
1271        final Intent intent = mIntentPool.get(context, ApiService.class);
1272        intent.putExtra(PARAM_OP, OP_AUDIO_TRACK_SET_VOLUME);
1273        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1274        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, audioTrackId);
1275        intent.putExtra(PARAM_VOLUME, volumePercentage);
1276
1277        startCommand(context, intent);
1278    }
1279
1280    /**
1281     * Mute/unmute the audio track
1282     *
1283     * @param context The context
1284     * @param projectPath The project path
1285     * @param audioTrackId The id of the audio track
1286     * @param muted true to mute the audio track
1287     */
1288    public static void setAudioTrackMute(Context context, String projectPath, String audioTrackId,
1289            boolean muted) {
1290        final Intent intent = mIntentPool.get(context, ApiService.class);
1291        intent.putExtra(PARAM_OP, OP_AUDIO_TRACK_SET_MUTE);
1292        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1293        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, audioTrackId);
1294        intent.putExtra(PARAM_MUTE, muted);
1295
1296        startCommand(context, intent);
1297    }
1298
1299    /**
1300     * Extract the audio track audio waveform
1301     *
1302     * @param context The context
1303     * @param projectPath The project path
1304     * @param audioTrackId The id of the audio track
1305     */
1306    public static void extractAudioTrackAudioWaveform(Context context, String projectPath,
1307            String audioTrackId) {
1308        if (isAudioTrackAudioWaveformPending(projectPath, audioTrackId)) {
1309            return;
1310        }
1311        final Intent intent = mIntentPool.get(context, ApiService.class);
1312        intent.putExtra(PARAM_OP, OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM);
1313        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
1314        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, audioTrackId);
1315
1316        startCommand(context, intent);
1317    }
1318
1319    /**
1320     * Check if extract audio waveform is pending for the specified audio track
1321     *
1322     * @param projectPath The project path
1323     * @param audioTrackId The audio track id
1324     *
1325     * @return true if the extract audio waveform is pending
1326     */
1327    public static boolean isAudioTrackAudioWaveformPending(String projectPath,
1328            String audioTrackId) {
1329        for (Intent intent : mPendingIntents.values()) {
1330            int op = intent.getIntExtra(PARAM_OP, -1);
1331            if (op == OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM) {
1332                String pp = intent.getStringExtra(PARAM_PROJECT_PATH);
1333                if (pp.equals(projectPath)) {
1334                    String mid = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
1335                    if (mid.equals(audioTrackId)) {
1336                        return true;
1337                    }
1338                }
1339            }
1340        }
1341
1342        return false;
1343    }
1344
1345    /**
1346     * Start the service (if it is not running) with the specified Intent
1347     *
1348     * @param context The context
1349     * @param intent The intent
1350     *
1351     * @return The request id of the pending request
1352     */
1353    private static String startCommand(Context context, Intent intent) {
1354        final String requestId = StringUtils.randomString(8);
1355        intent.putExtra(PARAM_REQUEST_ID, requestId);
1356        mPendingIntents.put(requestId, intent);
1357
1358        context.startService(intent);
1359
1360        final String projectPath = intent.getStringExtra(PARAM_PROJECT_PATH);
1361        if (projectPath != null) {
1362            final boolean projectEdited = isProjectBeingEdited(projectPath);
1363            if (projectEdited) {
1364                for (ApiServiceListener listener : mListeners) {
1365                    listener.onProjectEditState(projectPath, projectEdited);
1366                }
1367            }
1368        }
1369
1370        return requestId;
1371    }
1372
1373    @Override
1374    public void onCreate() {
1375        super.onCreate();
1376        mHandler = new Handler(Looper.getMainLooper());
1377
1378        mVideoThread = new IntentProcessor("VideoServiceThread");
1379        mVideoThread.start();
1380
1381        mAudioThread = new IntentProcessor("AudioServiceThread");
1382        mAudioThread.start();
1383
1384        mThumbnailThread = new IntentProcessor("ThumbnailServiceThread");
1385        mThumbnailThread.start();
1386    }
1387
1388    @Override
1389    public int onStartCommand(final Intent intent, int flags, int startId) {
1390        final int op = intent.getIntExtra(PARAM_OP, -1);
1391        switch(op) {
1392            case OP_VIDEO_EDITOR_LOAD_PROJECTS:
1393            case OP_VIDEO_EDITOR_CREATE:
1394            case OP_VIDEO_EDITOR_LOAD:
1395            case OP_VIDEO_EDITOR_SAVE:
1396            case OP_VIDEO_EDITOR_RELEASE:
1397            case OP_VIDEO_EDITOR_DELETE:
1398            case OP_VIDEO_EDITOR_SET_ASPECT_RATIO:
1399            case OP_VIDEO_EDITOR_APPLY_THEME:
1400            case OP_VIDEO_EDITOR_EXPORT:
1401            case OP_VIDEO_EDITOR_CANCEL_EXPORT:
1402            case OP_VIDEO_EDITOR_EXPORT_STATUS:
1403
1404            case OP_MEDIA_ITEM_ADD_VIDEO_URI:
1405            case OP_MEDIA_ITEM_ADD_IMAGE_URI:
1406            case OP_MEDIA_ITEM_MOVE:
1407            case OP_MEDIA_ITEM_REMOVE:
1408            case OP_MEDIA_ITEM_SET_RENDERING_MODE:
1409            case OP_MEDIA_ITEM_SET_DURATION:
1410            case OP_MEDIA_ITEM_SET_BOUNDARIES:
1411            case OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM:
1412            case OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM_STATUS:
1413            case OP_MEDIA_ITEM_LOAD:
1414            case OP_MEDIA_ITEM_LOAD_STATUS:
1415
1416            case OP_EFFECT_ADD_COLOR:
1417            case OP_EFFECT_ADD_IMAGE_KEN_BURNS:
1418            case OP_EFFECT_REMOVE:
1419
1420            case OP_TRANSITION_INSERT_ALPHA:
1421            case OP_TRANSITION_INSERT_CROSSFADE:
1422            case OP_TRANSITION_INSERT_FADE_BLACK:
1423            case OP_TRANSITION_INSERT_SLIDING:
1424            case OP_TRANSITION_REMOVE:
1425            case OP_TRANSITION_SET_DURATION:
1426
1427            case OP_OVERLAY_ADD:
1428            case OP_OVERLAY_REMOVE:
1429            case OP_OVERLAY_SET_START_TIME:
1430            case OP_OVERLAY_SET_DURATION:
1431            case OP_OVERLAY_SET_ATTRIBUTES:
1432
1433            case OP_AUDIO_TRACK_ADD:
1434            case OP_AUDIO_TRACK_REMOVE:
1435            case OP_AUDIO_TRACK_SET_BOUNDARIES:
1436            case OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM:
1437            case OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM_STATUS: {
1438                mVideoThread.submit(intent);
1439                break;
1440            }
1441
1442            case OP_TRANSITION_GET_THUMBNAIL: {
1443                mThumbnailThread.submit(intent);
1444                break;
1445            }
1446
1447            case OP_MEDIA_ITEM_GET_THUMBNAILS: {
1448                final String projectPath = intent.getStringExtra(PARAM_PROJECT_PATH);
1449                final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
1450                final int token = intent.getIntExtra(PARAM_TOKEN, 0);
1451                // Cancel any pending thumbnail request for the same media item
1452                // but with a different token
1453                Iterator<Intent> intentQueueIterator = mThumbnailThread.getIntentQueueIterator();
1454                while (intentQueueIterator.hasNext()) {
1455                    Intent qIntent = intentQueueIterator.next();
1456                    int opi = qIntent.getIntExtra(PARAM_OP, -1);
1457                    String pp = qIntent.getStringExtra(PARAM_PROJECT_PATH);
1458                    String mid = qIntent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
1459                    int tk = qIntent.getIntExtra(PARAM_TOKEN, 0);
1460                    if (opi == op && pp.equals(projectPath) && mid.equals(mediaItemId)
1461                            && tk != token) {
1462                        boolean canceled = mThumbnailThread.cancel(qIntent);
1463                        if (canceled) {
1464                            logd("Canceled operation: " + op + " for media item" + mediaItemId);
1465                            mPendingIntents.remove(qIntent.getStringExtra(PARAM_REQUEST_ID));
1466                            mIntentPool.put(qIntent);
1467                        }
1468                        break;
1469                    }
1470                }
1471                mThumbnailThread.submit(intent);
1472                break;
1473            }
1474
1475            case OP_MEDIA_ITEM_SET_VOLUME:
1476            case OP_MEDIA_ITEM_SET_MUTE:
1477
1478            case OP_AUDIO_TRACK_SET_VOLUME:
1479            case OP_AUDIO_TRACK_SET_MUTE:
1480            case OP_AUDIO_TRACK_SET_LOOP:
1481            case OP_AUDIO_TRACK_SET_DUCK: {
1482                mAudioThread.submit(intent);
1483                break;
1484            }
1485
1486            default: {
1487                Log.e(TAG, "No thread assigned: " + op);
1488                break;
1489            }
1490        }
1491
1492        return START_NOT_STICKY;
1493    }
1494
1495    @Override
1496    public void onDestroy() {
1497        super.onDestroy();
1498
1499        if (mThumbnailThread != null) {
1500            mThumbnailThread.quit();
1501            mThumbnailThread = null;
1502        }
1503
1504        if (mAudioThread != null) {
1505            mAudioThread.quit();
1506            mAudioThread = null;
1507        }
1508
1509        if (mVideoThread != null) {
1510            mVideoThread.quit();
1511            mVideoThread = null;
1512        }
1513    }
1514
1515    @Override
1516    public IBinder onBind(Intent intent) {
1517        return null;
1518    }
1519
1520    /**
1521     * Process the intent
1522     *
1523     * @param intent The intent
1524     */
1525    public void processIntent(final Intent intent) {
1526        final int op = intent.getIntExtra(PARAM_OP, -1);
1527        VideoEditor videoEditor = null;
1528        try {
1529            final String projectPath = intent.getStringExtra(PARAM_PROJECT_PATH);
1530            // Check if the project path matches the current VideoEditor project
1531            switch (op) {
1532                case OP_VIDEO_EDITOR_LOAD_PROJECTS:
1533                case OP_VIDEO_EDITOR_CREATE:
1534                case OP_VIDEO_EDITOR_LOAD:
1535                case OP_VIDEO_EDITOR_DELETE: {
1536                    break;
1537                }
1538
1539                default: {
1540                    videoEditor = getVideoEditor(projectPath);
1541                    if (videoEditor == null) {
1542                        throw new IllegalArgumentException("Invalid project path: "
1543                                + projectPath + " for operation: " + op);
1544                    }
1545                    break;
1546                }
1547            }
1548
1549            switch (op) {
1550                case OP_VIDEO_EDITOR_LOAD_PROJECTS: {
1551                    logd("OP_LOAD_PROJECTS");
1552                    final List<VideoEditorProject> projects = new ArrayList<VideoEditorProject>();
1553                    final File dir = FileUtils.getProjectsRootDir(getApplicationContext());
1554                    if (dir != null) {
1555                        // Collect valid projects (project with valid metadata).
1556                        final File[] files = dir.listFiles();
1557                        if (files != null) {
1558                            for (int i = 0; i < files.length; i++) {
1559                                if (files[i].isDirectory()) {
1560                                    final String pp = files[i].getAbsolutePath();
1561                                    try {
1562                                        projects.add(VideoEditorProject.fromXml(null, pp));
1563                                    } catch (FileNotFoundException ex) {
1564                                        Log.w(TAG, "processIntent: Project file not found: " + pp);
1565                                        FileUtils.deleteDir(new File(pp));
1566                                    } catch (Exception ex) {
1567                                        ex.printStackTrace();
1568                                    }
1569                                }
1570                            }
1571
1572                            if (projects.size() > 0) {
1573                                // Sort the projects in order of "last saved"
1574                                Collections.sort(projects, new Comparator<VideoEditorProject>() {
1575                                    @Override
1576                                    public int compare(VideoEditorProject object1,
1577                                            VideoEditorProject object2) {
1578                                        if (object1.getLastSaved() > object2.getLastSaved()) {
1579                                            return -1;
1580                                        } else if (object1.getLastSaved() == object2.getLastSaved()) {
1581                                            return 0;
1582                                        } else {
1583                                            return 1;
1584                                        }
1585                                    }
1586                                });
1587                            }
1588                        }
1589                    }
1590
1591                    completeRequest(intent, videoEditor, null, projects, null, true);
1592                    break;
1593                }
1594
1595                case OP_VIDEO_EDITOR_CREATE: {
1596                    logd("OP_VIDEO_EDITOR_CREATE: " + projectPath);
1597
1598                    try {
1599                        // Release the current video editor if any
1600                        releaseEditor();
1601
1602                        videoEditor = VideoEditorFactory.create(projectPath);
1603
1604                        // Add the movies to the timeline
1605                        final String[] movies = intent.getStringArrayExtra(PARAM_MOVIES_FILENAMES);
1606                        for (int i = 0; i < movies.length; i++) {
1607                            final MediaItem mediaItem = new MediaVideoItem(videoEditor,
1608                                    generateId(), movies[i],
1609                                    MediaItem.RENDERING_MODE_BLACK_BORDER);
1610                            videoEditor.addMediaItem(mediaItem);
1611                        }
1612
1613                        // Add the photos to the timeline
1614                        final String[] photos = intent.getStringArrayExtra(PARAM_PHOTOS_FILENAMES);
1615                        for (int i = 0; i < photos.length; i++) {
1616                            final MediaItem mediaItem = new MediaImageItem(videoEditor,
1617                                    generateId(), photos[i],
1618                                    MediaItemUtils.getDefaultImageDuration(),
1619                                    MediaItem.RENDERING_MODE_BLACK_BORDER);
1620                            videoEditor.addMediaItem(mediaItem);
1621                        }
1622
1623                        // Create the project
1624                        final String projectName = intent.getStringExtra(PARAM_PROJECT_NAME);
1625                        final String themeId = intent.getStringExtra(PARAM_THEME);
1626                        if (themeId != null) {
1627                            applyThemeToMovie(videoEditor, themeId);
1628                        }
1629
1630                        // Set the aspect ratio to the aspect ratio of the first item
1631                        final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
1632                        if (mediaItems.size() > 0) {
1633                            videoEditor.setAspectRatio(mediaItems.get(0).getAspectRatio());
1634                        }
1635
1636                        // Create the video editor project
1637                        final VideoEditorProject videoProject = new VideoEditorProject(
1638                                videoEditor, projectPath, projectName, System.currentTimeMillis(),
1639                                0, 0, VideoEditorProject.DEFAULT_ZOOM_LEVEL, null, themeId, null);
1640                        videoProject.setMediaItems(copyMediaItems(
1641                                videoEditor.getAllMediaItems()));
1642                        videoProject.setAudioTracks(copyAudioTracks(
1643                                videoEditor.getAllAudioTracks()));
1644
1645                        // Make this project the current project
1646                        mVideoEditor = videoEditor;
1647                        mGeneratePreviewListener = new ServiceMediaProcessingProgressListener(
1648                                projectPath);
1649
1650                        completeRequest(intent, videoEditor, null, videoProject, null, false);
1651                        generatePreview(videoEditor, true);
1652                        completeRequest(intent);
1653                    } catch (Exception ex) {
1654                        if (videoEditor != null) {
1655                            videoEditor.release();
1656                            videoEditor = null;
1657                        }
1658                        throw ex;
1659                    }
1660
1661                    break;
1662                }
1663
1664                case OP_VIDEO_EDITOR_LOAD: {
1665                    videoEditor = releaseEditorNot(projectPath);
1666
1667                    if (videoEditor == null) {  // The old project was released.
1668                        logd("OP_VIDEO_EDITOR_LOAD: Loading: " + projectPath);
1669                        try {
1670                            // Load the project
1671                            videoEditor = VideoEditorFactory.load(projectPath, false);
1672
1673                            // Load the video editor project
1674                            final VideoEditorProject videoProject = VideoEditorProject.fromXml(
1675                                    videoEditor, projectPath);
1676                            videoProject.setMediaItems(copyMediaItems(
1677                                    videoEditor.getAllMediaItems()));
1678                            videoProject.setAudioTracks(copyAudioTracks(
1679                                    videoEditor.getAllAudioTracks()));
1680                            // Make this the current project
1681                            mVideoEditor = videoEditor;
1682                            mGeneratePreviewListener = new ServiceMediaProcessingProgressListener(
1683                                    projectPath);
1684
1685                            completeRequest(intent, videoEditor, null, videoProject, null, false);
1686                            generatePreview(videoEditor, true);
1687                            completeRequest(intent);
1688                        } catch (Exception ex) {
1689                            if (videoEditor != null) {
1690                                videoEditor.release();
1691                                videoEditor = null;
1692                            }
1693                            throw ex;
1694                        }
1695                    } else {  // The project is already loaded.
1696                        logd("OP_VIDEO_EDITOR_LOAD: Was already loaded: " + projectPath);
1697                        completeRequest(intent, videoEditor, null, null, null, true);
1698                    }
1699
1700                    break;
1701                }
1702
1703                case OP_VIDEO_EDITOR_SET_ASPECT_RATIO: {
1704                    logd("OP_VIDEO_EDITOR_SET_ASPECT_RATIO");
1705
1706                    videoEditor.setAspectRatio(intent.getIntExtra(PARAM_ASPECT_RATIO,
1707                            MediaProperties.ASPECT_RATIO_UNDEFINED));
1708
1709                    completeRequest(intent, videoEditor, null, null, null, false);
1710                    generatePreview(videoEditor, true);
1711                    completeRequest(intent);
1712                    break;
1713                }
1714
1715                case OP_VIDEO_EDITOR_APPLY_THEME: {
1716                    logd("OP_VIDEO_EDITOR_APPLY_THEME");
1717
1718                    // Apply the theme
1719                    applyThemeToMovie(videoEditor, intent.getStringExtra(PARAM_THEME));
1720
1721                    final List<MovieMediaItem> mediaItems =
1722                            copyMediaItems(videoEditor.getAllMediaItems());
1723                    final List<MovieAudioTrack> audioTracks =
1724                            copyAudioTracks(videoEditor.getAllAudioTracks());
1725
1726                    completeRequest(intent, videoEditor, null, mediaItems, audioTracks, false);
1727                    generatePreview(videoEditor, true);
1728                    completeRequest(intent);
1729                    break;
1730                }
1731
1732                case OP_VIDEO_EDITOR_EXPORT: {
1733                    logd("OP_VIDEO_EDITOR_EXPORT");
1734                    exportMovie(videoEditor, intent);
1735                    break;
1736                }
1737
1738                case OP_VIDEO_EDITOR_CANCEL_EXPORT: {
1739                    logd("OP_VIDEO_EDITOR_CANCEL_EXPORT");
1740                    videoEditor.cancelExport(intent.getStringExtra(PARAM_FILENAME));
1741                    completeRequest(intent, videoEditor, null, null, null, true);
1742                    break;
1743                }
1744
1745                case OP_VIDEO_EDITOR_EXPORT_STATUS: {
1746                    logd("OP_VIDEO_EDITOR_EXPORT_STATUS");
1747                    completeRequest(intent, videoEditor, null, null, null, true);
1748                    break;
1749                }
1750
1751                case OP_VIDEO_EDITOR_SAVE: {
1752                    logd("OP_VIDEO_EDITOR_SAVE: " + projectPath);
1753                    videoEditor.save();
1754
1755                    final VideoEditorProject videoProject = getProject(projectPath);
1756                    if (videoProject != null) {
1757                        videoProject.saveToXml();
1758                    }
1759
1760                    completeRequest(intent, videoEditor, null, null, null, true);
1761                    break;
1762                }
1763
1764                case OP_VIDEO_EDITOR_RELEASE: {
1765                    logd("OP_VIDEO_EDITOR_RELEASE: " + projectPath);
1766                    releaseEditor(projectPath);
1767                    completeRequest(intent, videoEditor, null, null, null, true);
1768                    break;
1769                }
1770
1771                case OP_VIDEO_EDITOR_DELETE: {
1772                    logd("OP_VIDEO_EDITOR_DELETE: " + projectPath);
1773                    releaseEditor(projectPath);
1774                    // Delete all the files and the project folder.
1775                    FileUtils.deleteDir(new File(projectPath));
1776                    completeRequest(intent, videoEditor, null, null, null, true);
1777                    break;
1778                }
1779
1780                case OP_MEDIA_ITEM_ADD_VIDEO_URI: {
1781                    logd("OP_MEDIA_ITEM_ADD_VIDEO_URI: " +
1782                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
1783                    final Uri data = intent.getParcelableExtra(PARAM_FILENAME);
1784                    String filename = null;
1785                    // Get the filename
1786                    Cursor cursor = null;
1787                    try {
1788                        cursor = getContentResolver().query(data,
1789                                new String[] {Video.Media.DATA}, null, null, null);
1790                        if (cursor.moveToFirst()) {
1791                            filename = cursor.getString(0);
1792                        }
1793                    } finally {
1794                        if (cursor != null) {
1795                            cursor.close();
1796                        }
1797                    }
1798
1799                    if (filename == null) {
1800                        throw new IllegalArgumentException("Media file not found: " + data);
1801                    }
1802
1803                    final MediaItem mediaItem = new MediaVideoItem(videoEditor,
1804                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
1805                            filename,
1806                            intent.getIntExtra(PARAM_MEDIA_ITEM_RENDERING_MODE, 0));
1807
1808                    videoEditor.insertMediaItem(mediaItem,
1809                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
1810
1811                    // If this is the first media item, change the aspect ratio
1812                    final Integer aspectRatio;
1813                    if (videoEditor.getAllMediaItems().size() == 1) {
1814                        videoEditor.setAspectRatio(mediaItem.getAspectRatio());
1815                        aspectRatio = videoEditor.getAspectRatio();
1816                    } else {
1817                        aspectRatio = null;
1818                    }
1819
1820                    // Apply the theme if any
1821                    final String themeId = intent.getStringExtra(PARAM_THEME);
1822                    if (themeId != null) {
1823                        applyThemeToMediaItem(videoEditor, themeId, mediaItem);
1824                    }
1825
1826                    completeRequest(intent, videoEditor, null, new MovieMediaItem(mediaItem),
1827                            aspectRatio, false);
1828                    generatePreview(videoEditor, true);
1829                    completeRequest(intent);
1830                    break;
1831                }
1832
1833                case OP_MEDIA_ITEM_ADD_IMAGE_URI: {
1834                    logd("OP_MEDIA_ITEM_ADD_IMAGE_URI: "
1835                        + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
1836
1837                    final Uri data = intent.getParcelableExtra(PARAM_FILENAME);
1838                    String filename = null;
1839                    // Get the filename
1840                    Cursor cursor = null;
1841                    try {
1842                        cursor = getContentResolver().query(data,
1843                                new String[] {Images.Media.DATA, Images.Media.MIME_TYPE},
1844                                null, null, null);
1845                        if (cursor.moveToFirst()) {
1846                            filename = cursor.getString(0);
1847                            final String mimeType = cursor.getString(1);
1848                            if ("image/jpeg".equals(mimeType)) {
1849                                try {
1850                                    final File outputFile = new File(projectPath,
1851                                            "gallery_image_" + generateId() + ".jpg");
1852                                    if (ImageUtils.transformJpeg(filename, outputFile)) {
1853                                        filename = outputFile.getAbsolutePath();
1854                                    }
1855                                } catch (Exception ex) {
1856                                    // Ignore the exception and continue
1857                                    Log.w(TAG, "Could not transform JPEG: " + filename, ex);
1858                                }
1859                            }
1860                        }
1861                    } finally {
1862                        if (cursor != null) {
1863                            cursor.close();
1864                        }
1865                    }
1866
1867                    if (filename == null) {
1868                        throw new IllegalArgumentException("Media file not found: " + data);
1869                    }
1870
1871                    final MediaItem mediaItem = new MediaImageItem(videoEditor,
1872                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
1873                            filename,
1874                            intent.getLongExtra(PARAM_DURATION, 0),
1875                            intent.getIntExtra(PARAM_MEDIA_ITEM_RENDERING_MODE, 0));
1876
1877                    videoEditor.insertMediaItem(mediaItem,
1878                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
1879
1880                    // If this is the first media item, change the aspect ratio
1881                    final Integer aspectRatio;
1882                    if (videoEditor.getAllMediaItems().size() == 1) {
1883                        videoEditor.setAspectRatio(mediaItem.getAspectRatio());
1884                        aspectRatio = videoEditor.getAspectRatio();
1885                    } else {
1886                        aspectRatio = null;
1887                    }
1888
1889                    // Apply the theme if any
1890                    final String themeId = intent.getStringExtra(PARAM_THEME);
1891                    if (themeId != null) {
1892                        applyThemeToMediaItem(videoEditor, themeId, mediaItem);
1893                    }
1894
1895                    completeRequest(intent, videoEditor, null, new MovieMediaItem(mediaItem),
1896                            aspectRatio, false);
1897                    generatePreview(videoEditor, true);
1898                    completeRequest(intent);
1899                    break;
1900                }
1901
1902                case OP_MEDIA_ITEM_LOAD: {
1903                    final Uri data = intent.getParcelableExtra(PARAM_FILENAME);
1904                    logd("OP_MEDIA_ITEM_LOAD: " + data);
1905                    final Intent requestIntent = intent;
1906                    new Thread() {
1907                        @Override
1908                        public void run() {
1909                            InputStream is = null;
1910                            FileOutputStream fos = null;
1911                            final File file = new File(projectPath, "download_" + generateId());
1912
1913                            final Intent statusIntent = mIntentPool.get();
1914                            statusIntent.putExtra(PARAM_OP, OP_MEDIA_ITEM_LOAD_STATUS);
1915                            statusIntent.putExtra(PARAM_PROJECT_PATH,
1916                                    requestIntent.getStringExtra(PARAM_PROJECT_PATH));
1917                            statusIntent.putExtra(PARAM_INTENT, requestIntent);
1918                            try {
1919                                is = getContentResolver().openInputStream(data);
1920                                // Save the input stream to a file
1921                                fos = new FileOutputStream(file);
1922                                final byte[] readBuffer = new byte[2048];
1923                                int readBytes;
1924                                while ((readBytes = is.read(readBuffer)) >= 0) {
1925                                    fos.write(readBuffer, 0, readBytes);
1926                                }
1927                            } catch (Exception ex) {
1928                                Log.e(TAG, "Cannot open input stream for: " + data);
1929                                statusIntent.putExtra(PARAM_EXCEPTION, ex);
1930                                file.delete();
1931                            } finally {
1932                                if (is != null) {
1933                                    try {
1934                                        is.close();
1935                                    } catch (IOException ex) {
1936                                        Log.e(TAG, "Cannot close input stream for: " + data);
1937                                    }
1938                                }
1939
1940                                if (fos != null) {
1941                                    try {
1942                                        fos.flush();
1943                                        fos.close();
1944                                    } catch (IOException ex) {
1945                                        Log.e(TAG, "Cannot close output stream for: " + data);
1946                                    }
1947                                }
1948                            }
1949
1950                            if (!statusIntent.hasExtra(PARAM_EXCEPTION)) {
1951                                final String filename = file.getAbsolutePath();
1952                                try {
1953                                    final String mimeType = getContentResolver().getType(data);
1954                                    if ("image/jpeg".equals(mimeType)) {
1955                                        final File outputFile = new File(projectPath,
1956                                                "download_" + generateId() + ".jpg");
1957                                        if (ImageUtils.transformJpeg(filename, outputFile)) {
1958                                            // Delete the downloaded file
1959                                            file.delete();
1960                                            statusIntent.putExtra(PARAM_FILENAME,
1961                                                    outputFile.getAbsolutePath());
1962                                        } else {
1963                                            statusIntent.putExtra(PARAM_FILENAME, filename);
1964                                        }
1965                                    } else {
1966                                        statusIntent.putExtra(PARAM_FILENAME, filename);
1967                                    }
1968                                } catch (Exception ex) {
1969                                    // Ignore the exception and continue
1970                                    Log.w(TAG, "Could not transform JPEG: " + filename, ex);
1971                                    statusIntent.putExtra(PARAM_FILENAME, filename);
1972                                }
1973                            }
1974
1975                            mVideoThread.submit(statusIntent);
1976                        }
1977                    }.start();
1978
1979                    break;
1980                }
1981
1982                case OP_MEDIA_ITEM_LOAD_STATUS: {
1983                    final Intent originalIntent = (Intent)intent.getParcelableExtra(PARAM_INTENT);
1984                    if (intent.hasExtra(PARAM_EXCEPTION)) { //
1985                        final Exception exception =
1986                            (Exception)intent.getSerializableExtra(PARAM_EXCEPTION);
1987                        completeRequest(intent, videoEditor, exception, null, originalIntent,
1988                                true);
1989                    } else {
1990                        completeRequest(intent, videoEditor, null,
1991                                intent.getStringExtra(PARAM_FILENAME), originalIntent, true);
1992                    }
1993                    break;
1994                }
1995
1996                case OP_MEDIA_ITEM_MOVE: {
1997                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
1998                    logd("OP_MEDIA_ITEM_MOVE: " + mediaItemId);
1999
2000                    // Determine the position of the media item we are moving
2001                    final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
2002                    final int mediaItemsCount = mediaItems.size();
2003                    int movedItemPosition = -1;
2004                    MediaItem movedMediaItem = null;
2005                    for (int i = 0; i < mediaItemsCount; i++) {
2006                        final MediaItem mi = mediaItems.get(i);
2007                        if (mi.getId().equals(mediaItemId)) {
2008                            movedMediaItem = mi;
2009                            movedItemPosition = i;
2010                            break;
2011                        }
2012                    }
2013
2014                    if (movedItemPosition == -1) {
2015                        throw new IllegalArgumentException("Moved MediaItem not found: " +
2016                                mediaItemId);
2017                    }
2018
2019                    final Transition beginTransition = movedMediaItem.getBeginTransition();
2020                    final Transition endTransition = movedMediaItem.getEndTransition();
2021
2022                    final String afterMediaItemId = intent.getStringExtra(
2023                            PARAM_RELATIVE_STORYBOARD_ITEM_ID);
2024                    videoEditor.moveMediaItem(mediaItemId, afterMediaItemId);
2025
2026                    // Apply the theme if any
2027                    final String themeId = intent.getStringExtra(PARAM_THEME);
2028                    if (themeId != null) {
2029                        // Apply the theme at the removed position
2030                        applyThemeAfterMove(videoEditor, themeId, movedMediaItem,
2031                                movedItemPosition, beginTransition, endTransition);
2032                    }
2033
2034                    final List<MovieMediaItem> mediaItemsCopy = copyMediaItems(mediaItems);
2035                    completeRequest(intent, videoEditor, null, mediaItemsCopy, null, false);
2036                    generatePreview(videoEditor, true);
2037                    completeRequest(intent);
2038                    break;
2039                }
2040
2041                case OP_MEDIA_ITEM_REMOVE: {
2042                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2043                    logd("OP_MEDIA_ITEM_REMOVE: " + mediaItemId);
2044
2045                    // Determine the position of the media item we are removing
2046                    final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
2047                    final int mediaItemsCount = mediaItems.size();
2048                    int removedItemPosition = -1;
2049                    MediaItem removedMediaItem = null;
2050                    for (int i = 0; i < mediaItemsCount; i++) {
2051                        if (mediaItems.get(i).getId().equals(mediaItemId)) {
2052                            removedMediaItem = mediaItems.get(i);
2053                            removedItemPosition = i;
2054                            break;
2055                        }
2056                    }
2057
2058                    if (removedMediaItem == null) {
2059                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
2060                    }
2061
2062                    final Transition beginTransition = removedMediaItem.getBeginTransition();
2063                    final Transition endTransition = removedMediaItem.getEndTransition();
2064
2065                    videoEditor.removeMediaItem(mediaItemId);
2066
2067                    // Apply the theme if any
2068                    MovieTransition movieTransition = null;
2069                    final String themeId = intent.getStringExtra(PARAM_THEME);
2070                    if (themeId != null && mediaItems.size() > 0) {
2071                        final Transition transition = applyThemeAfterRemove(videoEditor, themeId,
2072                                removedItemPosition, beginTransition, endTransition);
2073                        if (transition != null) {
2074                            movieTransition = new MovieTransition(transition);
2075                        }
2076                    }
2077
2078                    completeRequest(intent, videoEditor, null, movieTransition, null, false);
2079                    generatePreview(videoEditor, true);
2080                    completeRequest(intent);
2081                    break;
2082                }
2083
2084                case OP_MEDIA_ITEM_SET_RENDERING_MODE: {
2085                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2086                    logd("OP_MEDIA_ITEM_SET_RENDERING_MODE: " + mediaItemId);
2087
2088                    final MediaItem mediaItem = videoEditor.getMediaItem(mediaItemId);
2089                    if (mediaItem == null) {
2090                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
2091                    }
2092                    mediaItem.setRenderingMode(intent.getIntExtra(PARAM_MEDIA_ITEM_RENDERING_MODE,
2093                            MediaItem.RENDERING_MODE_BLACK_BORDER));
2094
2095                    completeRequest(intent, videoEditor, null, null, null, false);
2096                    generatePreview(videoEditor, true);
2097                    completeRequest(intent);
2098                    break;
2099                }
2100
2101                case OP_MEDIA_ITEM_SET_DURATION: {
2102                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2103                    logd("OP_MEDIA_ITEM_SET_DURATION: " + mediaItemId);
2104
2105                    final MediaImageItem mediaItem =
2106                        (MediaImageItem)videoEditor.getMediaItem(mediaItemId);
2107                    if (mediaItem == null) {
2108                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
2109                    }
2110
2111                    final long durationMs = intent.getLongExtra(PARAM_DURATION, 0);
2112                    mediaItem.setDuration(durationMs);
2113                    // Adjust all effects to the new duration
2114                    final List<Effect> effects = mediaItem.getAllEffects();
2115                    for (Effect effect : effects) {
2116                        effect.setDuration(durationMs);
2117                    }
2118
2119                    completeRequest(intent, videoEditor, null, new MovieMediaItem(mediaItem), null,
2120                            false);
2121                    generatePreview(videoEditor, true);
2122                    completeRequest(intent);
2123                    break;
2124                }
2125
2126                case OP_MEDIA_ITEM_SET_BOUNDARIES: {
2127                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2128                    final MediaVideoItem mediaItem =
2129                        (MediaVideoItem)videoEditor.getMediaItem(mediaItemId);
2130                    if (mediaItem == null) {
2131                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
2132                    }
2133
2134                    mediaItem.setExtractBoundaries(intent.getLongExtra(PARAM_BEGIN_BOUNDARY, 0),
2135                            intent.getLongExtra(PARAM_END_BOUNDARY, 0));
2136
2137                    final List<Overlay> overlays = mediaItem.getAllOverlays();
2138                    if (overlays.size() > 0) {
2139                        // Adjust the overlay
2140                        final Overlay overlay = overlays.get(0);
2141                        if (overlay.getStartTime() < mediaItem.getBoundaryBeginTime()) {
2142                            overlay.setStartTime(mediaItem.getBoundaryBeginTime());
2143                            overlay.setDuration(Math.min(overlay.getDuration(),
2144                                    mediaItem.getTimelineDuration()));
2145                        } else if (overlay.getStartTime() + overlay.getDuration() >
2146                                    mediaItem.getBoundaryEndTime()) {
2147                            overlay.setStartTime(Math.max(mediaItem.getBoundaryBeginTime(),
2148                                    mediaItem.getBoundaryEndTime() - overlay.getDuration()));
2149                            overlay.setDuration(mediaItem.getBoundaryEndTime() -
2150                                    overlay.getStartTime());
2151                        }
2152                    }
2153
2154                    completeRequest(intent, videoEditor, null, new MovieMediaItem(mediaItem), null,
2155                            false);
2156                    generatePreview(videoEditor, true);
2157                    completeRequest(intent);
2158                    break;
2159                }
2160
2161                case OP_MEDIA_ITEM_GET_THUMBNAILS: {
2162                    // Note that this command is executed in the thumbnail thread
2163                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2164                    logd("OP_MEDIA_ITEM_GET_THUMBNAILS: " + mediaItemId);
2165
2166                    final MediaItem mediaItem = videoEditor.getMediaItem(mediaItemId);
2167                    if (mediaItem == null) {
2168                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
2169                    }
2170
2171                    final VideoEditor ve = videoEditor; // Just to make it "final"
2172                    mediaItem.getThumbnailList(
2173                            intent.getIntExtra(PARAM_WIDTH, 0),
2174                            intent.getIntExtra(PARAM_HEIGHT, 0),
2175                            intent.getLongExtra(PARAM_START_TIME, 0),
2176                            intent.getLongExtra(PARAM_END_TIME, 0),
2177                            intent.getIntExtra(PARAM_COUNT, 0),
2178                            intent.getIntArrayExtra(PARAM_INDICES),
2179                            new GetThumbnailListCallback() {
2180                                public void onThumbnail(Bitmap bitmap, int index) {
2181                                    completeRequest(
2182                                            intent, ve, null, bitmap,
2183                                            Integer.valueOf(index), false);
2184                                }
2185                            }
2186                            );
2187
2188                    completeRequest(intent, videoEditor, null, null, null, true);
2189                    break;
2190                }
2191
2192                case OP_MEDIA_ITEM_SET_VOLUME: {
2193                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2194                    logd("OP_MEDIA_ITEM_SET_VOLUME: " + mediaItemId);
2195
2196                    final MediaItem mediaItem = videoEditor.getMediaItem(mediaItemId);
2197                    if (mediaItem != null && mediaItem instanceof MediaVideoItem) {
2198                        ((MediaVideoItem)mediaItem).setVolume(intent.getIntExtra(PARAM_VOLUME, 0));
2199
2200                        completeRequest(intent, videoEditor, null, null, null, false);
2201                        generatePreview(videoEditor, false);
2202                        completeRequest(intent);
2203                    } else {
2204                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
2205                    }
2206                    break;
2207                }
2208
2209                case OP_MEDIA_ITEM_SET_MUTE: {
2210                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2211                    logd("OP_MEDIA_ITEM_SET_MUTE: " + mediaItemId);
2212
2213                    final MediaItem mediaItem = videoEditor.getMediaItem(mediaItemId);
2214                    if (mediaItem != null && mediaItem instanceof MediaVideoItem) {
2215                        ((MediaVideoItem)mediaItem).setMute(intent.getBooleanExtra(PARAM_MUTE,
2216                                false));
2217
2218                        completeRequest(intent, videoEditor, null, null, null, false);
2219                        generatePreview(videoEditor, false);
2220                        completeRequest(intent);
2221                    } else {
2222                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
2223                    }
2224                    break;
2225                }
2226
2227                case OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM: {
2228                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2229                    logd("OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM: " + mediaItemId);
2230
2231                    final MediaItem mediaItem = videoEditor.getMediaItem(mediaItemId);
2232                    if (mediaItem != null && mediaItem instanceof MediaVideoItem) {
2233                        final MediaVideoItem movieMediaItem = ((MediaVideoItem)mediaItem);
2234                        final WaveformData waveformData = movieMediaItem.getWaveformData();
2235                        if (waveformData == null) {
2236                            extractMediaItemAudioWaveform(intent, videoEditor, movieMediaItem);
2237                            completeRequest(intent, videoEditor, null,
2238                                    movieMediaItem.getWaveformData(), null, true);
2239                        } else {
2240                            completeRequest(intent, videoEditor, null, waveformData, null, true);
2241                        }
2242                    } else {
2243                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
2244                    }
2245                    break;
2246                }
2247
2248                case OP_TRANSITION_INSERT_ALPHA: {
2249                    logd("OP_TRANSITION_INSERT_ALPHA: "
2250                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2251
2252                    final String afterMediaItemId =
2253                        intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID);
2254                    final MediaItem afterMediaItem;
2255                    if (afterMediaItemId != null) {
2256                        afterMediaItem = videoEditor.getMediaItem(afterMediaItemId);
2257                    } else {
2258                        afterMediaItem = null;
2259                    }
2260
2261                    final int maskRawResourceId = intent.getIntExtra(PARAM_TRANSITION_MASK,
2262                            R.raw.mask_contour);
2263
2264                    final MediaItem beforeMediaItem = nextMediaItem(videoEditor, afterMediaItemId);
2265                    final Transition transition = new TransitionAlpha(
2266                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
2267                            afterMediaItem, beforeMediaItem,
2268                            intent.getLongExtra(PARAM_DURATION, 0),
2269                            intent.getIntExtra(PARAM_TRANSITION_BEHAVIOR,
2270                                    Transition.BEHAVIOR_LINEAR),
2271                                    FileUtils.getMaskFilename(getApplicationContext(),
2272                                            maskRawResourceId),
2273                            intent.getIntExtra(PARAM_TRANSITION_BLENDING, 100),
2274                            intent.getBooleanExtra(PARAM_TRANSITION_INVERT, false));
2275                    videoEditor.addTransition(transition);
2276
2277                    completeRequest(intent, videoEditor, null, transition, null, false);
2278                    generatePreview(videoEditor, true);
2279                    completeRequest(intent);
2280                    break;
2281                }
2282
2283                case OP_TRANSITION_INSERT_CROSSFADE: {
2284                    logd("OP_TRANSITION_INSERT_CROSSFADE: "
2285                        + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2286
2287                    final String afterMediaItemId =
2288                        intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID);
2289                    final MediaItem afterMediaItem;
2290                    if (afterMediaItemId != null) {
2291                        afterMediaItem = videoEditor.getMediaItem(afterMediaItemId);
2292                    } else {
2293                        afterMediaItem = null;
2294                    }
2295
2296                    final MediaItem beforeMediaItem = nextMediaItem(videoEditor, afterMediaItemId);
2297                    final Transition transition = new TransitionCrossfade(
2298                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
2299                            afterMediaItem, beforeMediaItem,
2300                            intent.getLongExtra(PARAM_DURATION, 0),
2301                            intent.getIntExtra(PARAM_TRANSITION_BEHAVIOR,
2302                                    Transition.BEHAVIOR_LINEAR));
2303                    videoEditor.addTransition(transition);
2304
2305                    completeRequest(intent, videoEditor, null, transition, null, false);
2306                    generatePreview(videoEditor, true);
2307                    completeRequest(intent);
2308                    break;
2309                }
2310
2311                case OP_TRANSITION_INSERT_FADE_BLACK: {
2312                    logd("OP_TRANSITION_INSERT_FADE_TO_BLACK: "
2313                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2314
2315                    final String afterMediaItemId =
2316                        intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID);
2317                    final MediaItem afterMediaItem;
2318                    if (afterMediaItemId != null) {
2319                        afterMediaItem = videoEditor.getMediaItem(afterMediaItemId);
2320                    } else {
2321                        afterMediaItem = null;
2322                    }
2323
2324                    final MediaItem beforeMediaItem = nextMediaItem(videoEditor, afterMediaItemId);
2325                    final Transition transition = new TransitionFadeBlack(
2326                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
2327                            afterMediaItem, beforeMediaItem,
2328                            intent.getLongExtra(PARAM_DURATION, 0),
2329                            intent.getIntExtra(PARAM_TRANSITION_BEHAVIOR,
2330                                    Transition.BEHAVIOR_LINEAR));
2331                    videoEditor.addTransition(transition);
2332
2333                    completeRequest(intent, videoEditor, null, transition, null, false);
2334                    generatePreview(videoEditor, true);
2335                    completeRequest(intent);
2336                    break;
2337                }
2338
2339                case OP_TRANSITION_INSERT_SLIDING: {
2340                    logd("OP_TRANSITION_INSERT_SLIDING: "
2341                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2342
2343                    final String afterMediaItemId =
2344                        intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID);
2345                    final MediaItem afterMediaItem;
2346                    if (afterMediaItemId != null) {
2347                        afterMediaItem = videoEditor.getMediaItem(afterMediaItemId);
2348                    } else {
2349                        afterMediaItem = null;
2350                    }
2351
2352                    final MediaItem beforeMediaItem = nextMediaItem(videoEditor, afterMediaItemId);
2353                    final Transition transition = new TransitionSliding(
2354                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
2355                            afterMediaItem, beforeMediaItem,
2356                            intent.getLongExtra(PARAM_DURATION, 0),
2357                            intent.getIntExtra(PARAM_TRANSITION_BEHAVIOR,
2358                                    Transition.BEHAVIOR_LINEAR),
2359                                    intent.getIntExtra(PARAM_TRANSITION_DIRECTION,
2360                                            TransitionSliding.DIRECTION_RIGHT_OUT_LEFT_IN));
2361                    videoEditor.addTransition(transition);
2362
2363                    completeRequest(intent, videoEditor, null, transition, null, false);
2364                    generatePreview(videoEditor, true);
2365                    completeRequest(intent);
2366                    break;
2367                }
2368
2369                case OP_TRANSITION_REMOVE: {
2370                    logd("OP_TRANSITION_REMOVE: "
2371                        + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2372
2373                    videoEditor.removeTransition(intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2374
2375                    completeRequest(intent, videoEditor, null, null, null, false);
2376                    generatePreview(videoEditor, true);
2377                    completeRequest(intent);
2378                    break;
2379                }
2380
2381                case OP_TRANSITION_SET_DURATION: {
2382                    final String transitionId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2383                    logd("OP_TRANSITION_SET_DURATION: " + transitionId);
2384
2385                    final Transition transition = videoEditor.getTransition(transitionId);
2386                    if (transition == null) {
2387                        throw new IllegalArgumentException("Transition not found: " +
2388                                transitionId);
2389                    }
2390                    transition.setDuration(intent.getLongExtra(PARAM_DURATION, 0));
2391
2392                    completeRequest(intent, videoEditor, null, null, null, false);
2393                    generatePreview(videoEditor, true);
2394                    completeRequest(intent);
2395                    break;
2396                }
2397
2398                case OP_TRANSITION_GET_THUMBNAIL: {
2399                    final String transitionId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2400                    logd("OP_TRANSITION_GET_THUMBNAIL: " + transitionId);
2401
2402                    final Transition transition = videoEditor.getTransition(transitionId);
2403                    if (transition == null) {
2404                        throw new IllegalArgumentException("Transition not found: " +
2405                                transitionId);
2406                    }
2407
2408                    final int height = intent.getIntExtra(PARAM_HEIGHT, 0);
2409                    final MediaItem afterMediaItem = transition.getAfterMediaItem();
2410                    final Bitmap[] thumbnails = new Bitmap[2];
2411                    if (afterMediaItem != null) {
2412                        thumbnails[0] = afterMediaItem.getThumbnail(
2413                                (afterMediaItem.getWidth() * height) / afterMediaItem.getHeight(),
2414                                height, afterMediaItem.getTimelineDuration());
2415                    } else {
2416                        thumbnails[0] = null;
2417                    }
2418
2419                    final MediaItem beforeMediaItem = transition.getBeforeMediaItem();
2420                    if (beforeMediaItem != null) {
2421                        thumbnails[1] = beforeMediaItem.getThumbnail(
2422                                (beforeMediaItem.getWidth() * height) / beforeMediaItem.getHeight(),
2423                                height, 0);
2424                    } else {
2425                        thumbnails[1] = null;
2426                    }
2427
2428                    completeRequest(intent, videoEditor, null, thumbnails, null, true);
2429                    break;
2430                }
2431
2432                case OP_EFFECT_ADD_COLOR: {
2433                    logd("OP_EFFECT_ADD_COLOR: "
2434                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2435
2436                    final MediaItem mediaItem = videoEditor.getMediaItem(
2437                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2438                    if (mediaItem == null) {
2439                        throw new IllegalArgumentException("MediaItem not found: " +
2440                                intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2441                    }
2442
2443                    // Remove any existing effect
2444                    final List<Effect> effects = mediaItem.getAllEffects();
2445                    for (Effect effect : effects) {
2446                        mediaItem.removeEffect(effect.getId());
2447                    }
2448
2449                    final Effect effect = new EffectColor(mediaItem,
2450                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
2451                            intent.getLongExtra(PARAM_START_TIME, -1),
2452                            intent.getLongExtra(PARAM_DURATION, 0),
2453                            intent.getIntExtra(PARAM_EFFECT_TYPE, -1),
2454                            intent.getIntExtra(PARAM_EFFECT_PARAM, -1));
2455                    mediaItem.addEffect(effect);
2456
2457                    completeRequest(intent, videoEditor, null, new MovieEffect(effect), null,
2458                            false);
2459                    generatePreview(videoEditor, true);
2460                    completeRequest(intent);
2461                    break;
2462                }
2463
2464                case OP_EFFECT_ADD_IMAGE_KEN_BURNS: {
2465                    logd("OP_EFFECT_ADD_IMAGE_KEN_BURNS: "
2466                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2467
2468                    final MediaItem mediaItem = videoEditor.getMediaItem(
2469                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2470                    if (mediaItem == null) {
2471                        throw new IllegalArgumentException("MediaItem not found: " +
2472                                intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2473                    }
2474
2475                    // Remove any existing effect
2476                    final List<Effect> effects = mediaItem.getAllEffects();
2477                    for (Effect effect : effects) {
2478                        mediaItem.removeEffect(effect.getId());
2479                    }
2480
2481                    final Effect effect = new EffectKenBurns(mediaItem,
2482                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
2483                            (Rect)intent.getParcelableExtra(PARAM_MEDIA_ITEM_START_RECT),
2484                            (Rect)intent.getParcelableExtra(PARAM_MEDIA_ITEM_END_RECT),
2485                            intent.getLongExtra(PARAM_START_TIME, 0),
2486                            intent.getLongExtra(PARAM_DURATION, 0));
2487                    mediaItem.addEffect(effect);
2488
2489                    completeRequest(intent, videoEditor, null, new MovieEffect(effect), null,
2490                            false);
2491                    generatePreview(videoEditor, true);
2492                    completeRequest(intent);
2493                    break;
2494                }
2495
2496                case OP_EFFECT_REMOVE: {
2497                    logd("OP_EFFECT_REMOVE: " + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2498
2499                    final MediaItem mediaItem = videoEditor.getMediaItem(
2500                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2501                    if (mediaItem == null) {
2502                        throw new IllegalArgumentException("MediaItem not found: " +
2503                                intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2504                    }
2505
2506                    mediaItem.removeEffect(intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2507
2508                    completeRequest(intent, videoEditor, null, null, null, false);
2509                    generatePreview(videoEditor, true);
2510                    completeRequest(intent);
2511                    break;
2512                }
2513
2514                case OP_OVERLAY_ADD: {
2515                    logd("OP_OVERLAY_ADD: " + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2516
2517                    final MediaItem mediaItem = videoEditor.getMediaItem(
2518                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2519                    if (mediaItem == null) {
2520                        throw new IllegalArgumentException("MediaItem not found: " +
2521                                intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2522                    }
2523
2524                    // Remove any existing overlays
2525                    final List<Overlay> overlays = mediaItem.getAllOverlays();
2526                    for (Overlay overlay : overlays) {
2527                        mediaItem.removeOverlay(overlay.getId());
2528                    }
2529
2530                    final int scaledWidth, scaledHeight;
2531                    if (mediaItem instanceof MediaVideoItem) {
2532                        scaledWidth = ((MediaVideoItem)mediaItem).getWidth();
2533                        scaledHeight = ((MediaVideoItem)mediaItem).getHeight();
2534                    } else {
2535                        scaledWidth = ((MediaImageItem)mediaItem).getScaledWidth();
2536                        scaledHeight = ((MediaImageItem)mediaItem).getScaledHeight();
2537                    }
2538
2539                    final Bundle userAttributes = intent.getBundleExtra(PARAM_ATTRIBUTES);
2540
2541                    final int overlayType = MovieOverlay.getType(userAttributes);
2542                    final String title = MovieOverlay.getTitle(userAttributes);
2543                    final String subTitle = MovieOverlay.getSubtitle(userAttributes);
2544
2545                    final OverlayFrame overlay = new OverlayFrame(mediaItem,
2546                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
2547                            ImageUtils.buildOverlayBitmap(getApplicationContext(), null,
2548                                    overlayType, title, subTitle, scaledWidth, scaledHeight),
2549                            intent.getLongExtra(PARAM_START_TIME, -1),
2550                            intent.getLongExtra(PARAM_DURATION, 0));
2551
2552                    // Set the user attributes
2553                    for (String name : userAttributes.keySet()) {
2554                        if (MovieOverlay.getAttributeType(name).equals(Integer.class)) {
2555                            overlay.setUserAttribute(name,
2556                                    Integer.toString(userAttributes.getInt(name)));
2557                        } else { // Strings
2558                            overlay.setUserAttribute(name, userAttributes.getString(name));
2559                        }
2560                    }
2561                    mediaItem.addOverlay(overlay);
2562
2563                    completeRequest(intent, videoEditor, null, new MovieOverlay(overlay), null,
2564                            false);
2565                    generatePreview(videoEditor, true);
2566                    completeRequest(intent);
2567                    break;
2568                }
2569
2570                case OP_OVERLAY_REMOVE: {
2571                    logd("OP_OVERLAY_REMOVE: " + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2572
2573                    final MediaItem mediaItem = videoEditor.getMediaItem(
2574                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2575                    if (mediaItem == null) {
2576                        throw new IllegalArgumentException("MediaItem not found: " +
2577                                intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2578                    }
2579
2580                    mediaItem.removeOverlay(intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2581
2582                    completeRequest(intent, videoEditor, null, null, null, false);
2583                    generatePreview(videoEditor, true);
2584                    completeRequest(intent);
2585                    break;
2586                }
2587
2588                case OP_OVERLAY_SET_START_TIME: {
2589                    logd("OP_OVERLAY_SET_START_TIME: "
2590                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2591
2592                    final MediaItem mediaItem = videoEditor.getMediaItem(
2593                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2594                    if (mediaItem == null) {
2595                        throw new IllegalArgumentException("MediaItem not found: " +
2596                                intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2597                    }
2598
2599                    final Overlay overlay = mediaItem.getOverlay(
2600                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2601                    if (overlay == null) {
2602                        throw new IllegalArgumentException("Overlay not found: " +
2603                                intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2604                    }
2605
2606                    overlay.setStartTime(intent.getLongExtra(PARAM_START_TIME, 0));
2607
2608                    completeRequest(intent, videoEditor, null, null, null, false);
2609                    generatePreview(videoEditor, true);
2610                    completeRequest(intent);
2611                    break;
2612                }
2613
2614                case OP_OVERLAY_SET_DURATION: {
2615                    logd("OP_OVERLAY_SET_DURATION: "
2616                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2617
2618                    final MediaItem mediaItem = videoEditor.getMediaItem(
2619                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2620                    if (mediaItem == null) {
2621                        throw new IllegalArgumentException("MediaItem not found: " +
2622                                intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2623                    }
2624
2625                    final Overlay overlay = mediaItem.getOverlay(
2626                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2627                    if (overlay == null) {
2628                        throw new IllegalArgumentException("Overlay not found: " +
2629                                intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2630                    }
2631
2632                    overlay.setDuration(intent.getLongExtra(PARAM_DURATION, 0));
2633
2634                    completeRequest(intent, videoEditor, null, null, null, false);
2635                    generatePreview(videoEditor, true);
2636                    completeRequest(intent);
2637                    break;
2638                }
2639
2640                case OP_OVERLAY_SET_ATTRIBUTES: {
2641                    logd("OP_OVERLAY_SET_ATTRIBUTES: "
2642                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2643
2644                    final MediaItem mediaItem = videoEditor.getMediaItem(
2645                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2646                    if (mediaItem == null) {
2647                        throw new IllegalArgumentException("MediaItem not found: " +
2648                                intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID));
2649                    }
2650
2651                    final Overlay overlay = mediaItem.getOverlay(
2652                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2653                    if (overlay == null) {
2654                        throw new IllegalArgumentException("Overlay not found: " +
2655                                intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2656                    }
2657
2658                    final int scaledWidth, scaledHeight;
2659                    if (mediaItem instanceof MediaVideoItem) {
2660                        scaledWidth = ((MediaVideoItem)mediaItem).getWidth();
2661                        scaledHeight = ((MediaVideoItem)mediaItem).getHeight();
2662                    } else {
2663                        scaledWidth = ((MediaImageItem)mediaItem).getScaledWidth();
2664                        scaledHeight = ((MediaImageItem)mediaItem).getScaledHeight();
2665                    }
2666
2667                    final Bundle userAttributes = intent.getBundleExtra(PARAM_ATTRIBUTES);
2668                    final int overlayType = MovieOverlay.getType(userAttributes);
2669                    final String title = MovieOverlay.getTitle(userAttributes);
2670                    final String subTitle = MovieOverlay.getSubtitle(userAttributes);
2671
2672                    ((OverlayFrame)overlay).setBitmap(
2673                            ImageUtils.buildOverlayBitmap(getApplicationContext(), null,
2674                                    overlayType, title, subTitle, scaledWidth, scaledHeight));
2675
2676                    for (String name : userAttributes.keySet()) {
2677                        if (MovieOverlay.getAttributeType(name).equals(Integer.class)) {
2678                            overlay.setUserAttribute(name,
2679                                    Integer.toString(userAttributes.getInt(name)));
2680                        } else { // Strings
2681                            overlay.setUserAttribute(name, userAttributes.getString(name));
2682                        }
2683                    }
2684
2685                    completeRequest(intent, videoEditor, null, null, null, false);
2686                    generatePreview(videoEditor, true);
2687                    completeRequest(intent);
2688                    break;
2689                }
2690
2691                case OP_AUDIO_TRACK_ADD: {
2692                    logd("OP_AUDIO_TRACK_ADD: " + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2693
2694                    final Uri data = intent.getParcelableExtra(PARAM_FILENAME);
2695                    String filename = null;
2696                    // Get the filename
2697                    Cursor cursor = null;
2698                    try {
2699                        cursor = getContentResolver().query(data,
2700                                new String[] {Audio.Media.DATA}, null, null, null);
2701                        if (cursor.moveToFirst()) {
2702                            filename = cursor.getString(0);
2703                        }
2704                    } finally {
2705                        if (cursor != null) {
2706                            cursor.close();
2707                        }
2708                    }
2709
2710                    if (filename == null) {
2711                        throw new IllegalArgumentException("Media file not found: " + data);
2712                    }
2713
2714                    final AudioTrack audioTrack = new AudioTrack(videoEditor,
2715                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID), filename);
2716                    audioTrack.enableDucking(DUCK_THRESHOLD, DUCK_TRACK_VOLUME);
2717                    audioTrack.setVolume(DEFAULT_AUDIO_TRACK_VOLUME);
2718                    if (intent.getBooleanExtra(PARAM_LOOP, false)) {
2719                        audioTrack.enableLoop();
2720                    } else {
2721                        audioTrack.disableLoop();
2722                    }
2723
2724                    videoEditor.addAudioTrack(audioTrack);
2725
2726                    completeRequest(intent, videoEditor, null, new MovieAudioTrack(audioTrack),
2727                            null, false);
2728                    // This is needed to decode the audio file into a PCM file
2729                    generatePreview(videoEditor, false);
2730                    completeRequest(intent);
2731                    break;
2732                }
2733
2734                case OP_AUDIO_TRACK_REMOVE: {
2735                    logd("OP_AUDIO_TRACK_REMOVE: "
2736                            + intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2737
2738                    videoEditor.removeAudioTrack(intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID));
2739
2740                    completeRequest(intent, videoEditor, null, null, null, false);
2741                    generatePreview(videoEditor, false);
2742                    completeRequest(intent);
2743                    break;
2744                }
2745
2746                case OP_AUDIO_TRACK_SET_BOUNDARIES: {
2747                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2748                    logd("OP_AUDIO_TRACK_SET_BOUNDARIES: " + audioTrackId);
2749
2750                    final AudioTrack audioTrack = videoEditor.getAudioTrack(audioTrackId);
2751                    if (audioTrack == null) {
2752                        throw new IllegalArgumentException("AudioTrack not found: " +
2753                                audioTrackId);
2754                    }
2755
2756                    audioTrack.setExtractBoundaries(intent.getLongExtra(PARAM_BEGIN_BOUNDARY, 0),
2757                            intent.getLongExtra(PARAM_END_BOUNDARY, 0));
2758
2759                    completeRequest(intent, videoEditor, null, null, null, false);
2760                    generatePreview(videoEditor, false);
2761                    completeRequest(intent);
2762                    break;
2763                }
2764
2765                case OP_AUDIO_TRACK_SET_LOOP: {
2766                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2767                    logd("OP_AUDIO_TRACK_SET_LOOP: " + audioTrackId);
2768
2769                    final AudioTrack audioTrack = videoEditor.getAudioTrack(audioTrackId);
2770                    if (audioTrack == null) {
2771                        throw new IllegalArgumentException("AudioTrack not found: " +
2772                                audioTrackId);
2773                    }
2774
2775                    if (intent.getBooleanExtra(PARAM_LOOP, false)) {
2776                        audioTrack.enableLoop();
2777                    } else {
2778                        audioTrack.disableLoop();
2779                    }
2780
2781                    completeRequest(intent, videoEditor, null, null, null, false);
2782                    generatePreview(videoEditor, false);
2783                    completeRequest(intent);
2784                    break;
2785                }
2786
2787                case OP_AUDIO_TRACK_SET_DUCK: {
2788                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2789                    logd("OP_AUDIO_TRACK_SET_DUCK: " + audioTrackId);
2790
2791                    final AudioTrack audioTrack = videoEditor.getAudioTrack(audioTrackId);
2792                    if (audioTrack == null) {
2793                        throw new IllegalArgumentException("AudioTrack not found: " +
2794                                audioTrackId);
2795                    }
2796
2797                    if (intent.getBooleanExtra(PARAM_DUCK, false)) {
2798                        audioTrack.enableDucking(DUCK_THRESHOLD, DUCK_TRACK_VOLUME);
2799                    } else {
2800                        audioTrack.disableDucking();
2801                    }
2802
2803                    completeRequest(intent, videoEditor, null, null, null, false);
2804                    generatePreview(videoEditor, false);
2805                    completeRequest(intent);
2806                    break;
2807                }
2808
2809                case OP_AUDIO_TRACK_SET_VOLUME: {
2810                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2811                    logd("OP_AUDIO_TRACK_SET_VOLUME: " + audioTrackId);
2812
2813                    final AudioTrack audioTrack = videoEditor.getAudioTrack(audioTrackId);
2814                    if (audioTrack == null) {
2815                        throw new IllegalArgumentException("AudioTrack not found: " +
2816                                audioTrackId);
2817                    }
2818
2819                    audioTrack.setVolume(intent.getIntExtra(PARAM_VOLUME, 0));
2820
2821                    completeRequest(intent, videoEditor, null, null, null, false);
2822                    generatePreview(videoEditor, false);
2823                    completeRequest(intent);
2824                    break;
2825                }
2826
2827                case OP_AUDIO_TRACK_SET_MUTE: {
2828                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2829                    logd("OP_AUDIO_TRACK_SET_MUTE: " + audioTrackId);
2830
2831                    final AudioTrack audioTrack = videoEditor.getAudioTrack(audioTrackId);
2832                    if (audioTrack == null) {
2833                        throw new IllegalArgumentException("AudioTrack not found: " +
2834                                audioTrackId);
2835                    }
2836
2837                    audioTrack.setMute(intent.getBooleanExtra(PARAM_MUTE, false));
2838
2839                    completeRequest(intent, videoEditor, null, null, null, false);
2840                    generatePreview(videoEditor, false);
2841                    completeRequest(intent);
2842                    break;
2843                }
2844
2845                case OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM: {
2846                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
2847                    logd("OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM: " + audioTrackId);
2848
2849                    final AudioTrack audioTrack = videoEditor.getAudioTrack(audioTrackId);
2850                    if (audioTrack == null) {
2851                        throw new IllegalArgumentException("AudioTrack not found: " +
2852                                audioTrackId);
2853                    }
2854
2855                    final WaveformData waveformData = audioTrack.getWaveformData();
2856                    if (waveformData == null) {
2857                        extractAudioTrackAudioWaveform(intent, videoEditor, audioTrack);
2858                        completeRequest(intent, videoEditor, null, audioTrack.getWaveformData(),
2859                                null, true);
2860                    } else {
2861                        completeRequest(intent, videoEditor, null, waveformData, null, true);
2862                    }
2863                    break;
2864                }
2865
2866                default: {
2867                    throw new IllegalArgumentException("Unhandled operation: " + op);
2868                }
2869            }
2870        } catch (Exception ex) {
2871            ex.printStackTrace();
2872            completeRequest(intent, videoEditor, ex, null, null, true);
2873        }
2874    }
2875
2876    /**
2877     * Complete the request
2878     *
2879     * @param intent The intent
2880     * @param videoEditor The video editor
2881     * @param exception The exception
2882     * @param result The result object
2883     * @param extraResult The extra result object
2884     * @param finalize true if the request should be finalized
2885     */
2886    private void completeRequest(final Intent intent, final VideoEditor videoEditor,
2887            final Exception exception, final Object result, final Object extraResult,
2888            final boolean finalize) {
2889        mHandler.post(new Runnable() {
2890            @Override
2891            public void run() {
2892                onIntentProcessed(intent, videoEditor, result, extraResult, exception, finalize);
2893            }
2894        });
2895    }
2896
2897    /**
2898     * Complete the request
2899     *
2900     * @param intent The intent
2901     */
2902    private void completeRequest(final Intent intent) {
2903        mHandler.post (new Runnable() {
2904            @Override
2905            public void run() {
2906                finalizeRequest(intent);
2907                mIntentPool.put(intent);
2908            }
2909        });
2910    }
2911
2912    /**
2913     * Callback called after the specified intent is processed.
2914     *
2915     * @param intent The intent
2916     * @param videoEditor The VideoEditor on which the operation was performed
2917     * @param result The result object
2918     * @param extraResult The extra result object
2919     * @param ex The exception
2920     * @param finalize true if the request should be finalized
2921     */
2922    @SuppressWarnings("unchecked")
2923    public void onIntentProcessed(final Intent intent, VideoEditor videoEditor,
2924            Object result, Object extraResult, Exception ex, boolean finalize) {
2925
2926        final String projectPath = intent.getStringExtra(PARAM_PROJECT_PATH);
2927        final int op = intent.getIntExtra(PARAM_OP, -1);
2928        switch (op) {
2929            case OP_VIDEO_EDITOR_LOAD_PROJECTS: {
2930                if (finalize) {
2931                    finalizeRequest(intent);
2932                }
2933
2934                final List<VideoEditorProject> projects = (List<VideoEditorProject>)result;
2935                for (ApiServiceListener listener : mListeners) {
2936                    listener.onProjectsLoaded(projects, ex);
2937                }
2938
2939                break;
2940            }
2941
2942            case OP_VIDEO_EDITOR_CREATE: {
2943                if (finalize) {
2944                    finalizeRequest(intent);
2945                }
2946
2947                // Release the old project
2948                if (mVideoProject != null) {
2949                    mVideoProject.release();
2950                    mVideoProject = null;
2951                }
2952
2953                if (ex != null) {
2954                    FileUtils.deleteDir(new File(projectPath));
2955                } else {
2956                    mVideoProject = (VideoEditorProject)result;
2957                }
2958
2959                for (ApiServiceListener listener : mListeners) {
2960                    listener.onVideoEditorCreated(projectPath, mVideoProject,
2961                            videoEditor != null ? videoEditor.getAllMediaItems() : null,
2962                            videoEditor != null ? videoEditor.getAllAudioTracks() : null, ex);
2963                }
2964
2965                break;
2966            }
2967
2968            case OP_VIDEO_EDITOR_LOAD: {
2969                if (finalize) {
2970                    finalizeRequest(intent);
2971                }
2972
2973                if (result != null) { // A new project was created
2974                    if (mVideoProject != null) {
2975                        mVideoProject.release();
2976                        mVideoProject = null;
2977                    }
2978                    mVideoProject = (VideoEditorProject)result;
2979                }
2980
2981                for (ApiServiceListener listener : mListeners) {
2982                    listener.onVideoEditorLoaded(projectPath, mVideoProject,
2983                            ex == null ? videoEditor.getAllMediaItems() : null,
2984                            ex == null ? videoEditor.getAllAudioTracks() : null, ex);
2985                }
2986
2987                break;
2988            }
2989
2990            case OP_VIDEO_EDITOR_SET_ASPECT_RATIO: {
2991                if (finalize) {
2992                    finalizeRequest(intent);
2993                }
2994
2995                final int aspectRatio = intent.getIntExtra(PARAM_ASPECT_RATIO,
2996                        MediaProperties.ASPECT_RATIO_UNDEFINED);
2997                if (ex == null) {
2998                    final VideoEditorProject videoProject = getProject(projectPath);
2999                    if (videoProject != null) {
3000                        videoProject.setAspectRatio(aspectRatio);
3001                    }
3002                }
3003
3004                for (ApiServiceListener listener : mListeners) {
3005                    listener.onVideoEditorAspectRatioSet(projectPath, aspectRatio, ex);
3006                }
3007
3008                break;
3009            }
3010
3011            case OP_VIDEO_EDITOR_APPLY_THEME: {
3012                if (finalize) {
3013                    finalizeRequest(intent);
3014                }
3015
3016                final String theme = intent.getStringExtra(PARAM_THEME);
3017                if (ex == null) {
3018                    final VideoEditorProject videoProject = getProject(projectPath);
3019                    if (videoProject != null) {
3020                        videoProject.setTheme(theme);
3021                        videoProject.setMediaItems((List<MovieMediaItem>)result);
3022                        videoProject.setAudioTracks((List<MovieAudioTrack>)extraResult);
3023                    }
3024                }
3025
3026                for (ApiServiceListener listener : mListeners) {
3027                    listener.onVideoEditorThemeApplied(projectPath, theme, ex);
3028                }
3029
3030                break;
3031            }
3032
3033            case OP_VIDEO_EDITOR_GENERATE_PREVIEW_PROGRESS: {
3034                final String className = intent.getStringExtra(PARAM_ATTRIBUTES);
3035                final String itemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3036                final int action = intent.getIntExtra(PARAM_ACTION,
3037                        VideoEditor.MediaProcessingProgressListener.ACTION_DECODE);
3038                final int progress = intent.getIntExtra(PARAM_PROGRESS_VALUE, 0);
3039
3040                for (ApiServiceListener listener : mListeners) {
3041                    listener.onVideoEditorGeneratePreviewProgress(projectPath, className, itemId,
3042                            action, progress);
3043                }
3044
3045                break;
3046            }
3047
3048            case OP_VIDEO_EDITOR_EXPORT: {
3049                // The finalizeRequest() call and listener callbacks are done in
3050                // OP_VIDEO_EDITOR_EXPORT_STATUS intent handling (where we are
3051                // called originalIntent).
3052                break;
3053            }
3054
3055            case OP_VIDEO_EDITOR_CANCEL_EXPORT: {
3056                if (finalize) {
3057                    finalizeRequest(intent);
3058                }
3059
3060                for (ApiServiceListener listener : mListeners) {
3061                    listener.onVideoEditorExportCanceled(projectPath,
3062                            intent.getStringExtra(PARAM_FILENAME));
3063                }
3064                break;
3065            }
3066
3067            case OP_VIDEO_EDITOR_EXPORT_STATUS: {
3068                // This operation is for the service internal use only
3069                if (finalize) {
3070                    finalizeRequest(intent);
3071                }
3072
3073                final String filename = intent.getStringExtra(PARAM_FILENAME);
3074                if (intent.hasExtra(PARAM_EXCEPTION)) { // Complete
3075                    final Intent originalIntent = (Intent)intent.getParcelableExtra(PARAM_INTENT);
3076                    finalizeRequest(originalIntent);
3077                    mIntentPool.put(originalIntent);
3078
3079                    final Exception exception =
3080                        (Exception)intent.getSerializableExtra(PARAM_EXCEPTION);
3081                    final VideoEditorProject videoProject = getProject(projectPath);
3082                    final boolean cancelled = intent.getBooleanExtra(PARAM_CANCELLED, false);
3083                    if (!cancelled && videoProject != null && exception == null) {
3084                        final Uri uri = (Uri)intent.getParcelableExtra(PARAM_MOVIE_URI);
3085                        videoProject.addExportedMovieUri(uri);
3086                    }
3087
3088                    for (ApiServiceListener listener : mListeners) {
3089                        listener.onVideoEditorExportComplete(
3090                                projectPath, filename, exception, cancelled);
3091                    }
3092                } else { // Progress
3093                    for (ApiServiceListener listener : mListeners) {
3094                        listener.onVideoEditorExportProgress(projectPath, filename,
3095                                intent.getIntExtra(PARAM_PROGRESS_VALUE, -1));
3096                    }
3097
3098                    // The original request is still pending
3099                }
3100                break;
3101            }
3102
3103            case OP_VIDEO_EDITOR_SAVE: {
3104                if (finalize) {
3105                    finalizeRequest(intent);
3106                }
3107
3108                for (ApiServiceListener listener : mListeners) {
3109                    listener.onVideoEditorSaved(projectPath, ex);
3110                }
3111                break;
3112            }
3113
3114            case OP_VIDEO_EDITOR_RELEASE: {
3115                if (finalize) {
3116                    finalizeRequest(intent);
3117                }
3118
3119                final VideoEditorProject videoProject = getProject(projectPath);
3120                if (videoProject != null) {
3121                    videoProject.release();
3122                    if (mVideoProject == videoProject) {
3123                        mVideoProject = null;
3124                    }
3125                }
3126
3127                for (ApiServiceListener listener : mListeners) {
3128                    listener.onVideoEditorReleased(projectPath, ex);
3129                }
3130
3131                break;
3132            }
3133
3134            case OP_VIDEO_EDITOR_DELETE: {
3135                if (finalize) {
3136                    finalizeRequest(intent);
3137                }
3138
3139                final VideoEditorProject videoProject = getProject(projectPath);
3140                if (videoProject != null) {
3141                    videoProject.release();
3142                    if (mVideoProject == videoProject) {
3143                        mVideoProject = null;
3144                    }
3145                }
3146
3147                for (ApiServiceListener listener : mListeners) {
3148                    listener.onVideoEditorDeleted(projectPath, ex);
3149                }
3150
3151                break;
3152            }
3153
3154            case OP_MEDIA_ITEM_ADD_VIDEO_URI: {
3155                if (finalize) {
3156                    finalizeRequest(intent);
3157                }
3158
3159                final String afterMediaItemId =
3160                    intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3161
3162                final MovieMediaItem movieMediaItem = (MovieMediaItem)result;
3163                final VideoEditorProject videoProject = getProject(projectPath);
3164                if (videoProject != null) {
3165                    if (ex == null && extraResult != null) {
3166                        // The aspect ratio has changed
3167                        videoProject.setAspectRatio((Integer)extraResult);
3168                    }
3169
3170                    if (ex == null) {
3171                        videoProject.insertMediaItem(movieMediaItem, afterMediaItemId);
3172                    }
3173                }
3174
3175                for (ApiServiceListener listener : mListeners) {
3176                    listener.onMediaItemAdded(projectPath,
3177                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID), movieMediaItem,
3178                            afterMediaItemId, MediaVideoItem.class, (Integer)extraResult, ex);
3179                }
3180
3181                break;
3182            }
3183
3184            case OP_MEDIA_ITEM_ADD_IMAGE_URI: {
3185                if (finalize) {
3186                    finalizeRequest(intent);
3187                }
3188
3189                final String afterMediaItemId =
3190                    intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3191
3192                final MovieMediaItem movieMediaItem = (MovieMediaItem)result;
3193                final VideoEditorProject videoProject = getProject(projectPath);
3194                if (videoProject != null) {
3195                    if (ex == null && extraResult != null) {
3196                        // The aspect ratio has changed
3197                        videoProject.setAspectRatio((Integer)extraResult);
3198                    }
3199
3200                    if (ex == null) {
3201                        videoProject.insertMediaItem(movieMediaItem, afterMediaItemId);
3202                    }
3203                }
3204
3205                for (ApiServiceListener listener : mListeners) {
3206                    listener.onMediaItemAdded(projectPath,
3207                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID), movieMediaItem,
3208                            afterMediaItemId, MediaImageItem.class, (Integer)extraResult, ex);
3209                }
3210
3211                break;
3212            }
3213
3214            case OP_MEDIA_ITEM_LOAD: {
3215                // Note that this message is handled only if the download
3216                // cannot start.
3217                final Uri data = (Uri)intent.getParcelableExtra(PARAM_FILENAME);
3218                final String mimeType = intent.getStringExtra(PARAM_ATTRIBUTES);
3219                if (finalize) {
3220                    finalizeRequest(intent);
3221                }
3222
3223                for (ApiServiceListener listener : mListeners) {
3224                    listener.onMediaLoaded(projectPath, data, mimeType, null, ex);
3225                }
3226                break;
3227            }
3228
3229            case OP_MEDIA_ITEM_LOAD_STATUS: {
3230                if (finalize) {
3231                    finalizeRequest(intent);
3232                }
3233
3234                final Intent originalIntent = (Intent)intent.getParcelableExtra(PARAM_INTENT);
3235                final Uri data = (Uri)originalIntent.getParcelableExtra(PARAM_FILENAME);
3236                final String mimeType = originalIntent.getStringExtra(PARAM_ATTRIBUTES);
3237
3238                finalizeRequest(originalIntent);
3239                mIntentPool.put(originalIntent);
3240
3241                final String filename = intent.getStringExtra(PARAM_FILENAME);
3242
3243                if (ex == null && filename != null) {
3244                    final VideoEditorProject videoProject = getProject(projectPath);
3245                    videoProject.addDownload(data.toString(), mimeType, filename);
3246                }
3247
3248                for (ApiServiceListener listener : mListeners) {
3249                    listener.onMediaLoaded(projectPath, data, mimeType, filename, ex);
3250                }
3251                break;
3252            }
3253
3254            case OP_MEDIA_ITEM_MOVE: {
3255                if (finalize) {
3256                    finalizeRequest(intent);
3257                }
3258
3259                if (ex == null) {
3260                    final VideoEditorProject videoProject = getProject(projectPath);
3261                    if (videoProject != null) {
3262                        videoProject.setMediaItems((List<MovieMediaItem>)result);
3263                    }
3264                }
3265
3266                for (ApiServiceListener listener : mListeners) {
3267                    listener.onMediaItemMoved(projectPath,
3268                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
3269                            intent.getStringExtra(PARAM_RELATIVE_STORYBOARD_ITEM_ID), ex);
3270                }
3271
3272                break;
3273            }
3274
3275            case OP_MEDIA_ITEM_REMOVE: {
3276                if (finalize) {
3277                    finalizeRequest(intent);
3278                }
3279
3280                final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3281                final MovieTransition transition = (MovieTransition)result;
3282                if (ex == null) {
3283                    final VideoEditorProject videoProject = getProject(projectPath);
3284                    if (videoProject != null) {
3285                        videoProject.removeMediaItem(mediaItemId, transition);
3286                    }
3287                }
3288
3289                for (ApiServiceListener listener : mListeners) {
3290                    listener.onMediaItemRemoved(projectPath, mediaItemId, transition, ex);
3291                }
3292
3293                break;
3294            }
3295
3296            case OP_MEDIA_ITEM_SET_RENDERING_MODE: {
3297                if (finalize) {
3298                    finalizeRequest(intent);
3299                }
3300
3301                final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3302                final int renderingMode = intent.getIntExtra(PARAM_MEDIA_ITEM_RENDERING_MODE,
3303                        MediaItem.RENDERING_MODE_BLACK_BORDER);
3304
3305                final VideoEditorProject videoProject = getProject(projectPath);
3306                if (videoProject != null) {
3307                    final MovieMediaItem mediaItem = videoProject.getMediaItem(mediaItemId);
3308                    if (mediaItem != null) {
3309                        videoProject.setClean(false);
3310                        if (ex == null) {
3311                            mediaItem.setRenderingMode(renderingMode);
3312                        } else {
3313                            mediaItem.setAppRenderingMode(mediaItem.getRenderingMode());
3314                        }
3315                    }
3316                }
3317
3318                for (ApiServiceListener listener : mListeners) {
3319                    listener.onMediaItemRenderingModeSet(projectPath, mediaItemId, renderingMode,
3320                            ex);
3321                }
3322
3323                break;
3324            }
3325
3326            case OP_MEDIA_ITEM_SET_DURATION: {
3327                if (finalize) {
3328                    finalizeRequest(intent);
3329                }
3330
3331                final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3332
3333                final VideoEditorProject videoProject = getProject(projectPath);
3334                if (videoProject != null) {
3335                    if (ex == null) {
3336                        videoProject.updateMediaItem((MovieMediaItem)result);
3337                    } else {
3338                        final MovieMediaItem oldMediaItem = videoProject.getMediaItem(mediaItemId);
3339                        if (oldMediaItem != null) {
3340                            videoProject.setClean(false);
3341                            oldMediaItem.setAppExtractBoundaries(0, oldMediaItem.getDuration());
3342                        }
3343                    }
3344                }
3345
3346                for (ApiServiceListener listener : mListeners) {
3347                    listener.onMediaItemDurationSet(projectPath, mediaItemId,
3348                            intent.getLongExtra(PARAM_DURATION, 0), ex);
3349                }
3350
3351                break;
3352            }
3353
3354            case OP_MEDIA_ITEM_SET_BOUNDARIES: {
3355                if (finalize) {
3356                    finalizeRequest(intent);
3357                }
3358
3359                final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3360
3361                final VideoEditorProject videoProject = getProject(projectPath);
3362                if (videoProject != null) {
3363                    if (ex == null) {
3364                        final MovieMediaItem mediaItem = (MovieMediaItem)result;
3365                        videoProject.updateMediaItem(mediaItem);
3366                    } else {
3367                        final MovieMediaItem oldMediaItem = videoProject.getMediaItem(mediaItemId);
3368                        if (oldMediaItem != null) {
3369                            videoProject.setClean(false);
3370                            oldMediaItem.setAppExtractBoundaries(
3371                                    oldMediaItem.getBoundaryBeginTime(),
3372                                    oldMediaItem.getBoundaryEndTime());
3373                        }
3374                    }
3375                }
3376
3377                for (ApiServiceListener listener : mListeners) {
3378                    listener.onMediaItemBoundariesSet(projectPath, mediaItemId,
3379                            intent.getLongExtra(PARAM_BEGIN_BOUNDARY, 0),
3380                            intent.getLongExtra(PARAM_END_BOUNDARY, 0), ex);
3381                }
3382
3383                break;
3384            }
3385
3386            case OP_MEDIA_ITEM_GET_THUMBNAILS: {
3387                if (finalize) {
3388                    finalizeRequest(intent);
3389                    break;
3390                }
3391
3392                final Bitmap bitmap = (Bitmap)result;
3393                final int index = (Integer)extraResult;
3394                boolean used = false;
3395                for (ApiServiceListener listener : mListeners) {
3396                    used |= listener.onMediaItemThumbnail(projectPath,
3397                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
3398                            bitmap, index, intent.getIntExtra(PARAM_TOKEN, 0),
3399                            ex);
3400                }
3401
3402                if (used == false) {
3403                    if (bitmap != null) {
3404                        bitmap.recycle();
3405                    }
3406                }
3407
3408                break;
3409            }
3410
3411            case OP_MEDIA_ITEM_SET_VOLUME: {
3412                if (finalize) {
3413                    finalizeRequest(intent);
3414                }
3415
3416                final VideoEditorProject videoProject = getProject(projectPath);
3417                if (videoProject != null) {
3418                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3419                    final MovieMediaItem mediaItem = videoProject.getMediaItem(mediaItemId);
3420                    if (mediaItem != null) {
3421                        videoProject.setClean(false);
3422                        if (ex == null) {
3423                            mediaItem.setVolume(intent.getIntExtra(PARAM_VOLUME, 0));
3424                        } else {
3425                            mediaItem.setAppVolume(mediaItem.getVolume());
3426                        }
3427                    }
3428                }
3429
3430                break;
3431            }
3432
3433            case OP_MEDIA_ITEM_SET_MUTE: {
3434                if (finalize) {
3435                    finalizeRequest(intent);
3436                }
3437
3438                final VideoEditorProject videoProject = getProject(projectPath);
3439                if (videoProject != null) {
3440                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3441                    final MovieMediaItem mediaItem = videoProject.getMediaItem(mediaItemId);
3442                    if (mediaItem != null) {
3443                        videoProject.setClean(false);
3444                        if (ex == null) {
3445                            mediaItem.setMute(intent.getBooleanExtra(PARAM_MUTE, false));
3446                        } else {
3447                            mediaItem.setAppMute(mediaItem.isMuted());
3448                        }
3449                    }
3450                }
3451
3452                break;
3453            }
3454
3455            case OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM_STATUS: {
3456                if (finalize) {
3457                    finalizeRequest(intent);
3458                }
3459
3460                final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3461                final int progress = intent.getIntExtra(PARAM_PROGRESS_VALUE, 0);
3462
3463                for (ApiServiceListener listener : mListeners) {
3464                    listener.onMediaItemExtractAudioWaveformProgress(projectPath, mediaItemId,
3465                        progress);
3466                }
3467
3468                break;
3469            }
3470
3471            case OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM: {
3472                if (finalize) {
3473                    finalizeRequest(intent);
3474                }
3475
3476                final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3477
3478                final VideoEditorProject videoProject = getProject(projectPath);
3479                if (ex == null && videoProject != null) {
3480                    if (result != null) {
3481                        final MovieMediaItem mediaItem = videoProject.getMediaItem(mediaItemId);
3482                        if (mediaItem != null) {
3483                            videoProject.setClean(false);
3484                            mediaItem.setWaveformData((WaveformData)result);
3485                        }
3486                    }
3487                }
3488
3489                for (ApiServiceListener listener : mListeners) {
3490                    listener.onMediaItemExtractAudioWaveformComplete(projectPath, mediaItemId, ex);
3491                }
3492
3493                break;
3494            }
3495
3496            case OP_TRANSITION_INSERT_ALPHA:
3497            case OP_TRANSITION_INSERT_CROSSFADE:
3498            case OP_TRANSITION_INSERT_FADE_BLACK:
3499            case OP_TRANSITION_INSERT_SLIDING: {
3500                if (finalize) {
3501                    finalizeRequest(intent);
3502                }
3503
3504                final String afterMediaItemId = intent.getStringExtra(
3505                        PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3506
3507                final MovieTransition movieTransition;
3508                final VideoEditorProject videoProject = getProject(projectPath);
3509                if (videoProject != null) {
3510                    final Transition transition = (Transition)result;
3511                    // This is null for start transitions
3512                    if (ex == null) {
3513                        movieTransition = new MovieTransition(transition);
3514                        videoProject.addTransition(movieTransition, afterMediaItemId);
3515                    } else {
3516                        movieTransition = null;
3517                    }
3518                } else {
3519                    movieTransition = null;
3520                }
3521
3522                for (ApiServiceListener listener : mListeners) {
3523                    listener.onTransitionInserted(projectPath, movieTransition,
3524                            afterMediaItemId, ex);
3525                }
3526
3527                break;
3528            }
3529
3530            case OP_TRANSITION_REMOVE: {
3531                if (finalize) {
3532                    finalizeRequest(intent);
3533                }
3534
3535                final String transitionId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3536
3537                final VideoEditorProject videoProject = getProject(projectPath);
3538                if (videoProject != null) {
3539                    if (ex == null) {
3540                        videoProject.removeTransition(transitionId);
3541                    }
3542                }
3543
3544                for (ApiServiceListener listener : mListeners) {
3545                    listener.onTransitionRemoved(projectPath, transitionId, ex);
3546                }
3547
3548                break;
3549            }
3550
3551            case OP_TRANSITION_SET_DURATION: {
3552                if (finalize) {
3553                    finalizeRequest(intent);
3554                }
3555
3556                final String transitionId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3557                final long durationMs = intent.getLongExtra(PARAM_DURATION, 0);
3558
3559                final VideoEditorProject videoProject = getProject(projectPath);
3560                if (videoProject != null) {
3561                    final MovieTransition transition = videoProject.getTransition(transitionId);
3562                    if (transition != null) {
3563                        videoProject.setClean(false);
3564                        if (ex == null) {
3565                            transition.setDuration(durationMs);
3566                        } else {
3567                            transition.setAppDuration(transition.getDuration());
3568                        }
3569                    }
3570                }
3571
3572                for (ApiServiceListener listener : mListeners) {
3573                    listener.onTransitionDurationSet(projectPath, transitionId, durationMs, ex);
3574                }
3575
3576                break;
3577            }
3578
3579            case OP_TRANSITION_GET_THUMBNAIL: {
3580                if (finalize) {
3581                    finalizeRequest(intent);
3582                }
3583
3584                final Bitmap[] bitmaps = (Bitmap[])result;
3585                boolean used = false;
3586                for (ApiServiceListener listener : mListeners) {
3587                    used |= listener.onTransitionThumbnails(projectPath,
3588                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
3589                            bitmaps, ex);
3590                }
3591
3592                if (used == false) {
3593                    if (bitmaps != null) {
3594                        for (int i = 0; i < bitmaps.length; i++) {
3595                            if (bitmaps[i] != null) {
3596                                bitmaps[i].recycle();
3597                            }
3598                        }
3599                    }
3600                }
3601
3602                break;
3603            }
3604
3605            case OP_OVERLAY_ADD: {
3606                if (finalize) {
3607                    finalizeRequest(intent);
3608                }
3609
3610                final String mediaItemId = intent.getStringExtra(
3611                        PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3612
3613                final MovieOverlay movieOverlay = (MovieOverlay)result;
3614                final VideoEditorProject videoProject = getProject(projectPath);
3615                if (videoProject != null) {
3616                    if (ex == null) {
3617                        videoProject.addOverlay(mediaItemId, movieOverlay);
3618                    }
3619                }
3620
3621                for (ApiServiceListener listener : mListeners) {
3622                    listener.onOverlayAdded(projectPath, movieOverlay, mediaItemId, ex);
3623                }
3624
3625                break;
3626            }
3627
3628            case OP_OVERLAY_REMOVE: {
3629                if (finalize) {
3630                    finalizeRequest(intent);
3631                }
3632
3633                final String mediaItemId = intent.getStringExtra(
3634                        PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3635                final String overlayId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3636
3637                final VideoEditorProject videoProject = getProject(projectPath);
3638                if (videoProject != null) {
3639                    if (ex == null) {
3640                        videoProject.removeOverlay(mediaItemId, overlayId);
3641                    }
3642                }
3643
3644                for (ApiServiceListener listener : mListeners) {
3645                    listener.onOverlayRemoved(projectPath, overlayId, mediaItemId, ex);
3646                }
3647
3648                break;
3649            }
3650
3651            case OP_OVERLAY_SET_START_TIME: {
3652                if (finalize) {
3653                    finalizeRequest(intent);
3654                }
3655
3656                final String mediaItemId = intent.getStringExtra(
3657                        PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3658                final String overlayId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3659                final long startTimeMs = intent.getLongExtra(PARAM_START_TIME, 0);
3660
3661                final VideoEditorProject videoProject = getProject(projectPath);
3662                if (videoProject != null) {
3663                    final MovieOverlay overlay = videoProject.getOverlay(mediaItemId, overlayId);
3664                    if (overlay != null) {
3665                        videoProject.setClean(false);
3666                        if (ex == null) {
3667                            overlay.setStartTime(startTimeMs);
3668                        } else {
3669                            overlay.setAppStartTime(overlay.getStartTime());
3670                        }
3671                    }
3672                }
3673
3674                for (ApiServiceListener listener : mListeners) {
3675                    listener.onOverlayStartTimeSet(projectPath, overlayId, mediaItemId,
3676                            startTimeMs, ex);
3677                }
3678
3679                break;
3680            }
3681
3682            case OP_OVERLAY_SET_DURATION: {
3683                if (finalize) {
3684                    finalizeRequest(intent);
3685                }
3686
3687                final String mediaItemId = intent.getStringExtra(
3688                        PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3689                final String overlayId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3690                final long durationMs = intent.getLongExtra(PARAM_DURATION, 0);
3691
3692                final VideoEditorProject videoProject = getProject(projectPath);
3693                if (videoProject != null) {
3694                    final MovieOverlay overlay = videoProject.getOverlay(mediaItemId, overlayId);
3695                    if (overlay != null) {
3696                        videoProject.setClean(false);
3697                        if (ex == null) {
3698                            overlay.setDuration(durationMs);
3699                        } else {
3700                            overlay.setAppDuration(overlay.getDuration());
3701                        }
3702                    }
3703                }
3704
3705                for (ApiServiceListener listener : mListeners) {
3706                    listener.onOverlayDurationSet(projectPath, overlayId, mediaItemId,
3707                            durationMs, ex);
3708                }
3709
3710                break;
3711            }
3712
3713            case OP_OVERLAY_SET_ATTRIBUTES: {
3714                if (finalize) {
3715                    finalizeRequest(intent);
3716                }
3717
3718                final String mediaItemId = intent.getStringExtra(
3719                        PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3720                final String overlayId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3721                final Bundle userAttributes = intent.getBundleExtra(PARAM_ATTRIBUTES);
3722
3723                final VideoEditorProject videoProject = getProject(projectPath);
3724                if (videoProject != null) {
3725                    if (ex == null) {
3726                        final MovieOverlay overlay = videoProject.getOverlay(mediaItemId,
3727                                overlayId);
3728                        if (overlay != null) {
3729                            videoProject.setClean(false);
3730                            overlay.updateUserAttributes(userAttributes);
3731                        }
3732                    }
3733                }
3734
3735                for (ApiServiceListener listener : mListeners) {
3736                    listener.onOverlayUserAttributesSet(projectPath, overlayId, mediaItemId,
3737                            userAttributes, ex);
3738                }
3739
3740                break;
3741            }
3742
3743            case OP_EFFECT_ADD_COLOR:
3744            case OP_EFFECT_ADD_IMAGE_KEN_BURNS:{
3745                if (finalize) {
3746                    finalizeRequest(intent);
3747                }
3748
3749                final String mediaItemId = intent.getStringExtra(
3750                        PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3751
3752                final MovieEffect movieEffect = (MovieEffect)result;
3753                final VideoEditorProject videoProject = getProject(projectPath);
3754                if (videoProject != null) {
3755                    if (ex == null) {
3756                        videoProject.addEffect(mediaItemId, movieEffect);
3757                    }
3758                }
3759
3760                for (ApiServiceListener listener : mListeners) {
3761                    listener.onEffectAdded(projectPath, movieEffect, mediaItemId, ex);
3762                }
3763
3764                break;
3765            }
3766
3767            case OP_EFFECT_REMOVE: {
3768                if (finalize) {
3769                    finalizeRequest(intent);
3770                }
3771
3772                final String mediaItemId = intent.getStringExtra(
3773                        PARAM_RELATIVE_STORYBOARD_ITEM_ID);
3774                final String effectId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3775
3776                final VideoEditorProject videoProject = getProject(projectPath);
3777                if (videoProject != null) {
3778                    if (ex == null) {
3779                        videoProject.removeEffect(mediaItemId, effectId);
3780                    }
3781                }
3782
3783                for (ApiServiceListener listener : mListeners) {
3784                    listener.onEffectRemoved(projectPath, effectId, mediaItemId, ex);
3785                }
3786
3787                break;
3788            }
3789
3790            case OP_AUDIO_TRACK_ADD: {
3791                if (finalize) {
3792                    finalizeRequest(intent);
3793                }
3794
3795                final MovieAudioTrack movieAudioTrack = (MovieAudioTrack)result;
3796                final VideoEditorProject videoProject = getProject(projectPath);
3797                if (videoProject != null) {
3798                    if (ex == null) {
3799                        videoProject.addAudioTrack(movieAudioTrack);
3800                    }
3801                }
3802
3803                for (ApiServiceListener listener : mListeners) {
3804                    listener.onAudioTrackAdded(projectPath, movieAudioTrack, ex);
3805                }
3806
3807                break;
3808            }
3809
3810            case OP_AUDIO_TRACK_REMOVE: {
3811                if (finalize) {
3812                    finalizeRequest(intent);
3813                }
3814
3815                final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3816
3817                final VideoEditorProject videoProject = getProject(projectPath);
3818                if (videoProject != null) {
3819                    if (ex == null) {
3820                        videoProject.removeAudioTrack(audioTrackId);
3821                    }
3822                }
3823
3824                for (ApiServiceListener listener : mListeners) {
3825                    listener.onAudioTrackRemoved(projectPath, audioTrackId, ex);
3826                }
3827
3828                break;
3829            }
3830
3831            case OP_AUDIO_TRACK_SET_BOUNDARIES: {
3832                if (finalize) {
3833                    finalizeRequest(intent);
3834                }
3835
3836                final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3837                final long beginBoundary = intent.getLongExtra(PARAM_BEGIN_BOUNDARY, 0);
3838                final long endBoundary = intent.getLongExtra(PARAM_END_BOUNDARY, 0);
3839
3840                final VideoEditorProject videoProject = getProject(projectPath);
3841                if (videoProject != null) {
3842                    if (ex == null) {
3843                        final MovieAudioTrack audioTrack =
3844                            videoProject.getAudioTrack(audioTrackId);
3845                        if (audioTrack != null) {
3846                            videoProject.setClean(false);
3847                            audioTrack.setExtractBoundaries(beginBoundary, endBoundary);
3848                        }
3849                    }
3850                }
3851
3852                for (ApiServiceListener listener : mListeners) {
3853                    listener.onAudioTrackBoundariesSet(projectPath, audioTrackId,
3854                            beginBoundary, endBoundary, ex);
3855                }
3856
3857                break;
3858            }
3859
3860            case OP_AUDIO_TRACK_SET_LOOP: {
3861                if (finalize) {
3862                    finalizeRequest(intent);
3863                }
3864
3865                final VideoEditorProject videoProject = getProject(projectPath);
3866                if (videoProject != null) {
3867                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3868                    final MovieAudioTrack audioTrack = videoProject.getAudioTrack(audioTrackId);
3869                    if (audioTrack != null) {
3870                        videoProject.setClean(false);
3871                        if (ex == null) {
3872                            audioTrack.enableLoop(intent.getBooleanExtra(PARAM_LOOP, false));
3873                        } else {
3874                            audioTrack.enableAppLoop(audioTrack.isLooping());
3875                        }
3876                    }
3877                }
3878
3879                break;
3880            }
3881
3882            case OP_AUDIO_TRACK_SET_DUCK: {
3883                if (finalize) {
3884                    finalizeRequest(intent);
3885                }
3886
3887                final VideoEditorProject videoProject = getProject(projectPath);
3888                if (videoProject != null) {
3889                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3890                    final MovieAudioTrack audioTrack = videoProject.getAudioTrack(audioTrackId);
3891                    if (audioTrack != null) {
3892                        videoProject.setClean(false);
3893                        if (ex == null) {
3894                            audioTrack.enableDucking(intent.getBooleanExtra(PARAM_DUCK, false));
3895                        } else {
3896                            audioTrack.enableAppDucking(audioTrack.isDuckingEnabled());
3897                        }
3898                    }
3899                }
3900
3901                break;
3902            }
3903
3904            case OP_AUDIO_TRACK_SET_VOLUME: {
3905                if (finalize) {
3906                    finalizeRequest(intent);
3907                }
3908
3909                final VideoEditorProject videoProject = getProject(projectPath);
3910                if (videoProject != null) {
3911                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3912                    final MovieAudioTrack audioTrack = videoProject.getAudioTrack(audioTrackId);
3913                    if (audioTrack != null) {
3914                        videoProject.setClean(false);
3915                        if (ex == null) {
3916                            audioTrack.setVolume(intent.getIntExtra(PARAM_VOLUME, 0));
3917                        } else {
3918                            audioTrack.setAppVolume(audioTrack.getVolume());
3919                        }
3920                    }
3921                }
3922
3923                break;
3924            }
3925
3926            case OP_AUDIO_TRACK_SET_MUTE: {
3927                if (finalize) {
3928                    finalizeRequest(intent);
3929                }
3930
3931                final VideoEditorProject videoProject = getProject(projectPath);
3932                if (videoProject != null) {
3933                    final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3934                    final MovieAudioTrack audioTrack = videoProject.getAudioTrack(audioTrackId);
3935                    if (audioTrack != null) {
3936                        videoProject.setClean(false);
3937                        if (ex == null) {
3938                            audioTrack.setMute(intent.getBooleanExtra(PARAM_MUTE, false));
3939                        } else {
3940                            audioTrack.setAppMute(audioTrack.isMuted());
3941                        }
3942                    }
3943                }
3944
3945                break;
3946            }
3947
3948            case OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM_STATUS: {
3949                if (finalize) {
3950                    finalizeRequest(intent);
3951                }
3952
3953                final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3954                final int progress = intent.getIntExtra(PARAM_PROGRESS_VALUE, 0);
3955
3956                for (ApiServiceListener listener : mListeners) {
3957                    listener.onAudioTrackExtractAudioWaveformProgress(projectPath, audioTrackId,
3958                            progress);
3959                }
3960
3961                break;
3962            }
3963
3964            case OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM: {
3965                if (finalize) {
3966                    finalizeRequest(intent);
3967                }
3968
3969                final String audioTrackId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
3970
3971                final VideoEditorProject videoProject = getProject(projectPath);
3972                if (ex == null && videoProject != null) {
3973                    if (result != null) {
3974                        final MovieAudioTrack audioTrack =
3975                            videoProject.getAudioTrack(audioTrackId);
3976                        if (audioTrack != null) {
3977                            videoProject.setClean(false);
3978                            audioTrack.setWaveformData((WaveformData)result);
3979                        }
3980                    }
3981                }
3982
3983                for (ApiServiceListener listener : mListeners) {
3984                    listener.onAudioTrackExtractAudioWaveformComplete(projectPath,
3985                            audioTrackId, ex);
3986                }
3987
3988                break;
3989            }
3990
3991            default: {
3992                if (finalize) {
3993                    finalizeRequest(intent);
3994                }
3995                break;
3996            }
3997        }
3998
3999        if (finalize) {
4000            mIntentPool.put(intent);
4001        }
4002    }
4003
4004    /**
4005     * Finalizes a request. Calls the listeners that are interested in project status
4006     * change and stops this service if there are no more pending intents.
4007     *
4008     * @param intent The intent that just completed
4009     */
4010    private void finalizeRequest(Intent intent) {
4011        mPendingIntents.remove(intent.getStringExtra(PARAM_REQUEST_ID));
4012
4013        final String projectPath = intent.getStringExtra(PARAM_PROJECT_PATH);
4014        if (projectPath != null) {
4015            final boolean projectEdited = isProjectBeingEdited(projectPath);
4016            if (projectEdited == false) {
4017                for (ApiServiceListener listener : mListeners) {
4018                    listener.onProjectEditState(projectPath, projectEdited);
4019                }
4020            }
4021        }
4022
4023        if (mPendingIntents.size() == 0) {
4024            // Cancel the current timer if any. Extend the timeout by 5000 ms.
4025            mHandler.removeCallbacks(mStopRunnable);
4026
4027            // Start a timer which will stop the service if the queue of
4028            // pending intent will be empty at that time.
4029            // This prevents the service from starting & stopping too often.
4030            mHandler.postDelayed(mStopRunnable, 5000);
4031            logd("completeRequest: Stopping service in 5000 ms");
4032        }
4033    }
4034
4035    /**
4036     * Checks if the current project is the project specified by the specified path.
4037     *
4038     * @param projectPath The project path
4039     *
4040     * @return The video editor project
4041     */
4042    private VideoEditorProject getProject(String projectPath) {
4043        if (mVideoProject != null) {
4044            if (mVideoProject.getPath().equals(projectPath)) {
4045                return mVideoProject;
4046            }
4047        }
4048
4049        return null;
4050    }
4051
4052    /**
4053     * Check if the current editor is the project specified by the specified path
4054     *
4055     * @param projectPath The project path
4056     *
4057     * @return The video editor
4058     */
4059    private synchronized VideoEditor getVideoEditor(String projectPath) {
4060        if (mVideoEditor != null) {
4061            if (mVideoEditor.getPath().equals(projectPath)) {
4062                return mVideoEditor;
4063            }
4064        }
4065
4066        return null;
4067    }
4068
4069    /**
4070     * Release the editor
4071     */
4072    private synchronized void releaseEditor() {
4073        if (mVideoEditor != null) {
4074            logd("releaseEditor (current): " + mVideoEditor.getPath());
4075            mVideoEditor.release();
4076            mVideoEditor = null;
4077            mGeneratePreviewListener = null;
4078
4079            System.gc();
4080        }
4081    }
4082
4083    /**
4084     * Release the specified editor
4085     *
4086     * @param projectPath The project path
4087     */
4088    private synchronized void releaseEditor(String projectPath) {
4089        if (mVideoEditor != null) {
4090            if (mVideoEditor.getPath().equals(projectPath)) {
4091                logd("releaseEditor: " + projectPath);
4092                mVideoEditor.release();
4093                mVideoEditor = null;
4094                mGeneratePreviewListener = null;
4095
4096                System.gc();
4097            }
4098        }
4099    }
4100
4101    /**
4102     * Release the current editor if it is *not* the current editor
4103     *
4104     * @param projectPath The project path
4105     *
4106     * @return The current video editor
4107     */
4108    private synchronized VideoEditor releaseEditorNot(String projectPath) {
4109        if (mVideoEditor != null) {
4110            if (!mVideoEditor.getPath().equals(projectPath)) {
4111                logd("releaseEditorNot: " + mVideoEditor.getPath());
4112                mVideoEditor.release();
4113                mVideoEditor = null;
4114                mGeneratePreviewListener = null;
4115
4116                System.gc();
4117            }
4118        }
4119
4120        return mVideoEditor;
4121    }
4122
4123    /**
4124     * Generate the preview
4125     *
4126     * @param videoEditor The video editor
4127     * @param updatePreviewFrame true to show preview frame when done
4128     */
4129    private void generatePreview(VideoEditor videoEditor, boolean updatePreviewFrame) {
4130        try {
4131            videoEditor.generatePreview(mGeneratePreviewListener);
4132            if (mGeneratePreviewListener != null) {
4133                // This is the last callback which is always fired last to
4134                // let the UI know that generate preview completed
4135                mGeneratePreviewListener.onProgress(null,
4136                        updatePreviewFrame ? ACTION_UPDATE_FRAME : ACTION_NO_FRAME_UPDATE, 100);
4137            }
4138        } catch (Exception ex) {
4139            ex.printStackTrace();
4140        }
4141    }
4142
4143    /**
4144     * Exports a movie in a distinct worker thread.
4145     *
4146     * @param videoEditor The video editor
4147     * @param intent The intent
4148     */
4149    private void exportMovie(final VideoEditor videoEditor, final Intent intent) {
4150        mExportCancelled = false;
4151        new Thread() {
4152            @Override
4153            public void run() {
4154                final String filename = intent.getStringExtra(PARAM_FILENAME);
4155                final int height = intent.getIntExtra(PARAM_HEIGHT, -1);
4156                final int bitrate = intent.getIntExtra(PARAM_BITRATE, -1);
4157
4158                // Create the export status Intent
4159                final Intent statusIntent = mIntentPool.get();
4160                statusIntent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_EXPORT_STATUS);
4161                statusIntent.putExtra(PARAM_PROJECT_PATH, intent.getStringExtra(
4162                        PARAM_PROJECT_PATH));
4163                statusIntent.putExtra(PARAM_FILENAME, filename);
4164                statusIntent.putExtra(PARAM_INTENT, intent);
4165                Exception resultException = null;
4166
4167                try {
4168                    videoEditor.export(filename, height, bitrate, new ExportProgressListener() {
4169                        @Override
4170                        public void onProgress(VideoEditor videoEditor, String filename,
4171                                int progress) {
4172                            final Intent progressIntent = mIntentPool.get();
4173                            progressIntent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_EXPORT_STATUS);
4174                            progressIntent.putExtra(PARAM_PROJECT_PATH, intent.getStringExtra(
4175                                    PARAM_PROJECT_PATH));
4176                            progressIntent.putExtra(PARAM_FILENAME, filename);
4177                            progressIntent.putExtra(PARAM_INTENT, intent);
4178                            progressIntent.putExtra(PARAM_PROGRESS_VALUE, progress);
4179                            mVideoThread.submit(progressIntent);
4180                            logv("Export progress: " + progress + " for: " + filename);
4181                        }
4182                    });
4183
4184                    statusIntent.putExtra(PARAM_CANCELLED, mExportCancelled);
4185                    if (!mExportCancelled) {
4186                        if (new File(filename).exists()) {
4187                            statusIntent.putExtra(PARAM_MOVIE_URI, exportToGallery(filename));
4188                        } else {
4189                            resultException = new IllegalStateException("Export file does not exist: " + filename);
4190                        }
4191                        logv("Export complete for: " + filename);
4192                    } else {
4193                        logv("Export cancelled by user, file name: " + filename);
4194                    }
4195                } catch (Exception ex) {
4196                    logv("Export error for: " + filename);
4197                    ex.printStackTrace();
4198                    resultException = ex;
4199                }
4200
4201                // Complete the request
4202                statusIntent.putExtra(PARAM_EXCEPTION, resultException);
4203                mVideoThread.submit(statusIntent);
4204            }
4205        }.start();
4206    }
4207
4208    /**
4209     * Extract the audio waveform of a media item
4210     *
4211     * @param intent The original Intent
4212     * @param videoEditor The video editor
4213     * @param mediaItem The media item
4214     */
4215    private void extractMediaItemAudioWaveform(final Intent intent, final VideoEditor videoEditor,
4216            final MediaVideoItem mediaItem) throws IOException {
4217        mediaItem.extractAudioWaveform(
4218            new ExtractAudioWaveformProgressListener() {
4219            @Override
4220            public void onProgress(int progress) {
4221                final Intent progressIntent = mIntentPool.get();
4222                progressIntent.putExtra(PARAM_OP, OP_MEDIA_ITEM_EXTRACT_AUDIO_WAVEFORM_STATUS);
4223                progressIntent.putExtra(PARAM_PROJECT_PATH,
4224                        intent.getStringExtra(PARAM_PROJECT_PATH));
4225                progressIntent.putExtra(PARAM_INTENT, intent);
4226                progressIntent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItem.getId());
4227                progressIntent.putExtra(PARAM_PROGRESS_VALUE, progress);
4228
4229                completeRequest(progressIntent, videoEditor, null, null, null, true);
4230            }
4231        });
4232    }
4233
4234    /**
4235     * Extract the audio waveform of an AudioTrack
4236     *
4237     * @param intent The original Intent
4238     * @param videoEditor The video editor
4239     * @param audioTrack The audio track
4240     */
4241    private void extractAudioTrackAudioWaveform(final Intent intent, final VideoEditor videoEditor,
4242            final AudioTrack audioTrack) throws IOException {
4243        audioTrack.extractAudioWaveform(
4244            new ExtractAudioWaveformProgressListener() {
4245            @Override
4246            public void onProgress(int progress) {
4247                final Intent progressIntent = mIntentPool.get();
4248                progressIntent.putExtra(PARAM_OP,
4249                        OP_AUDIO_TRACK_EXTRACT_AUDIO_WAVEFORM_STATUS);
4250                progressIntent.putExtra(PARAM_PROJECT_PATH,
4251                        intent.getStringExtra(PARAM_PROJECT_PATH));
4252                progressIntent.putExtra(PARAM_INTENT, intent);
4253                progressIntent.putExtra(PARAM_STORYBOARD_ITEM_ID, audioTrack.getId());
4254                progressIntent.putExtra(PARAM_PROGRESS_VALUE, progress);
4255
4256                completeRequest(progressIntent, videoEditor, null, null, null, true);
4257            }
4258        });
4259    }
4260
4261    /**
4262     * Get the media item following the specified media item
4263     *
4264     * @param videoEditor The video editor
4265     * @param mediaItemId The media item id
4266     * @return The next media item
4267     */
4268    private static MediaItem nextMediaItem(VideoEditor videoEditor, String mediaItemId) {
4269        final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
4270        if (mediaItemId == null) {
4271            if (mediaItems.size() > 0) {
4272                return mediaItems.get(0);
4273            }
4274        } else {
4275            final int mediaItemCount = mediaItems.size();
4276            for (int i = 0; i < mediaItemCount; i++) {
4277                MediaItem mi = mediaItems.get(i);
4278                if (mi.getId().equals(mediaItemId)) {
4279                    if (i < mediaItemCount - 1) {
4280                        return mediaItems.get(i + 1);
4281                    } else {
4282                        break;
4283                    }
4284                }
4285            }
4286        }
4287
4288        return null;
4289    }
4290
4291    /**
4292     * Export the movie to the Gallery
4293     *
4294     * @param filename The filename
4295     * @return The video MediaStore URI
4296     */
4297    private Uri exportToGallery(String filename) {
4298        // Save the name and description of a video in a ContentValues map.
4299        final ContentValues values = new ContentValues(2);
4300        values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
4301        values.put(MediaStore.Video.Media.DATA, filename);
4302        // Add a new record (identified by uri)
4303        final Uri uri = getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
4304                values);
4305        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
4306                Uri.parse("file://"+ filename)));
4307        return uri;
4308    }
4309
4310    /**
4311     * Apply a theme to the entire movie. This method shall be used when the
4312     * theme is changing.
4313     *
4314     * @param videoEditor The video editor
4315     * @param themeId The theme id
4316     */
4317    private void applyThemeToMovie(VideoEditor videoEditor, String themeId) throws IOException {
4318        final Context context = getApplicationContext();
4319        final MovieTheme theme = MovieTheme.getTheme(context, themeId);
4320        final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
4321
4322        // Add the transitions
4323        final int mediaItemsCount = mediaItems.size();
4324        if (mediaItemsCount > 0) {
4325            // Remove all the transitions
4326            for (int i = 0; i < mediaItemsCount; i++) {
4327                final MediaItem mi = mediaItems.get(i);
4328                if (i == 0) {
4329                    final Transition beginTransition = mi.getBeginTransition();
4330                    if (beginTransition != null) {
4331                        videoEditor.removeTransition(beginTransition.getId());
4332                    }
4333                }
4334
4335                final Transition endTransition = mi.getEndTransition();
4336                if (endTransition != null) {
4337                    videoEditor.removeTransition(endTransition.getId());
4338                }
4339            }
4340
4341            // Add the begin transition to the first media item
4342            final MovieTransition beginMovieTransition = theme.getBeginTransition();
4343            if (beginMovieTransition != null) {
4344                final MediaItem firstMediaItem = mediaItems.get(0);
4345                videoEditor.addTransition(
4346                        beginMovieTransition.buildTransition(context, null, firstMediaItem));
4347            }
4348
4349            // Add the mid transitions
4350            final MovieTransition midMovieTransition = theme.getMidTransition();
4351            if (midMovieTransition != null) {
4352                for (int i = 0; i < mediaItemsCount - 1; i++) {
4353                    videoEditor.addTransition(
4354                            midMovieTransition.buildTransition(context,
4355                                    mediaItems.get(i), mediaItems.get(i + 1)));
4356                }
4357            }
4358
4359            // Add the end transition to the last media item
4360            final MovieTransition endMovieTransition = theme.getEndTransition();
4361            if (endMovieTransition != null) {
4362                final MediaItem lastMediaItem = mediaItems.get(mediaItemsCount - 1);
4363                videoEditor.addTransition(
4364                        endMovieTransition.buildTransition(context, lastMediaItem, null));
4365            }
4366        }
4367
4368        // Add the overlay
4369        final MovieOverlay movieOverlay = theme.getOverlay();
4370        if (movieOverlay != null && mediaItemsCount > 0) {
4371            // Remove all the overlay for the first media item
4372            final MediaItem mediaItem = mediaItems.get(0);
4373            final List<Overlay> overlays = mediaItem.getAllOverlays();
4374            if (overlays.size() > 0) {
4375                mediaItem.removeOverlay(overlays.get(0).getId());
4376            }
4377
4378            // Add the new overlay
4379            final int scaledWidth, scaledHeight;
4380            if (mediaItem instanceof MediaVideoItem) {
4381                scaledWidth = ((MediaVideoItem)mediaItem).getWidth();
4382                scaledHeight = ((MediaVideoItem)mediaItem).getHeight();
4383            } else {
4384                scaledWidth = ((MediaImageItem)mediaItem).getScaledWidth();
4385                scaledHeight = ((MediaImageItem)mediaItem).getScaledHeight();
4386            }
4387
4388            final Overlay overlay = new OverlayFrame(mediaItem, generateId(),
4389                    ImageUtils.buildOverlayBitmap(getApplicationContext(), null,
4390                            movieOverlay.getType(), movieOverlay.getTitle(),
4391                            movieOverlay.getSubtitle(), scaledWidth, scaledHeight),
4392                            movieOverlay.getStartTime(), movieOverlay.getDuration());
4393
4394            // Set the user attributes
4395            final Bundle userAttributes = movieOverlay.buildUserAttributes();
4396            for (String name : userAttributes.keySet()) {
4397                if (MovieOverlay.getAttributeType(name).equals(Integer.class)) {
4398                    overlay.setUserAttribute(name,
4399                            Integer.toString(userAttributes.getInt(name)));
4400                } else { // Strings
4401                    overlay.setUserAttribute(name, userAttributes.getString(name));
4402                }
4403            }
4404            mediaItem.addOverlay(overlay);
4405        }
4406
4407        final MovieAudioTrack at = theme.getAudioTrack();
4408        if (at != null) {
4409            // Remove all audio tracks
4410            final List<AudioTrack> audioTracks = videoEditor.getAllAudioTracks();
4411            while (audioTracks.size() > 0) {
4412                videoEditor.removeAudioTrack(audioTracks.get(0).getId());
4413            }
4414
4415            // Add the new audio track
4416            final AudioTrack audioTrack = new AudioTrack(videoEditor, generateId(),
4417                    FileUtils.getAudioTrackFilename(context, at.getRawResourceId()));
4418
4419            // Enable looping if necessary
4420            if (at.isLooping()) {
4421                audioTrack.enableLoop();
4422            }
4423
4424            // Enable ducking
4425            audioTrack.enableDucking(DUCK_THRESHOLD, DUCK_TRACK_VOLUME);
4426            audioTrack.setVolume(DEFAULT_AUDIO_TRACK_VOLUME);
4427            videoEditor.addAudioTrack(audioTrack);
4428        }
4429    }
4430
4431    /**
4432     * Apply a theme
4433     *
4434     * @param videoEditor The video editor
4435     * @param themeId The theme id
4436     * @param mediaItem The mediaItem
4437     */
4438    private void applyThemeToMediaItem(VideoEditor videoEditor, String themeId,
4439            MediaItem mediaItem) throws IOException {
4440        final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
4441        final int mediaItemsCount = mediaItems.size();
4442        if (mediaItemsCount == 0) {
4443            return;
4444        }
4445
4446        // We would only add transitions if the transitions don't exist
4447        final Transition beginTransition = mediaItem.getBeginTransition();
4448        final Transition endTransition = mediaItem.getEndTransition();
4449
4450        final Context context = getApplicationContext();
4451        final MovieTheme theme = MovieTheme.getTheme(context, themeId);
4452
4453        final MediaItem firstMediaItem = mediaItems.get(0);
4454        if (beginTransition == null) {
4455            // Add the begin transition
4456            final MovieTransition beginMovieTransition = theme.getBeginTransition();
4457            if (beginMovieTransition != null) {
4458                if (firstMediaItem == mediaItem) {
4459                    videoEditor.addTransition(
4460                            beginMovieTransition.buildTransition(context, null, mediaItem));
4461                }
4462            }
4463        }
4464
4465        // Add the mid transitions
4466        final MovieTransition midMovieTransition = theme.getMidTransition();
4467        if (midMovieTransition != null) {
4468            for (int i = 0; i < mediaItemsCount; i++) {
4469                final MediaItem mi = mediaItems.get(i);
4470                if (mi == mediaItem) {
4471                    if (i > 0) { // Not the first one
4472                        if (beginTransition == null) {
4473                            // Add transition before this media item
4474                            videoEditor.addTransition(midMovieTransition.buildTransition(context,
4475                                    mediaItems.get(i - 1), mi));
4476                        }
4477                    }
4478
4479                    if (i < mediaItemsCount - 1) { // Not the last one
4480                        if (endTransition == null) {
4481                            // Add the transition after this media item
4482                            videoEditor.addTransition(midMovieTransition.buildTransition(context,
4483                                        mi, mediaItems.get(i + 1)));
4484                        }
4485                    }
4486                    break;
4487                }
4488            }
4489        }
4490
4491        if (endTransition == null) {
4492            // Add the end transition to the last media item
4493            final MovieTransition endMovieTransition = theme.getEndTransition();
4494            final MediaItem lastMediaItem = mediaItems.get(mediaItemsCount - 1);
4495            if (endMovieTransition != null && lastMediaItem == mediaItem) {
4496                videoEditor.addTransition(
4497                        endMovieTransition.buildTransition(context, lastMediaItem, null));
4498            }
4499        }
4500
4501        // Add the overlay
4502        final MovieOverlay movieOverlay = theme.getOverlay();
4503        if (movieOverlay != null) {
4504            if (firstMediaItem == mediaItem) {
4505                // Remove the overlay
4506                final List<Overlay> overlays = mediaItem.getAllOverlays();
4507                if (overlays.size() > 0) {
4508                    mediaItem.removeOverlay(overlays.get(0).getId());
4509                }
4510
4511                // Add the new overlay
4512                final int scaledWidth, scaledHeight;
4513                if (mediaItem instanceof MediaVideoItem) {
4514                    scaledWidth = ((MediaVideoItem)mediaItem).getWidth();
4515                    scaledHeight = ((MediaVideoItem)mediaItem).getHeight();
4516                } else {
4517                    scaledWidth = ((MediaImageItem)mediaItem).getScaledWidth();
4518                    scaledHeight = ((MediaImageItem)mediaItem).getScaledHeight();
4519                }
4520
4521                final Overlay overlay = new OverlayFrame(mediaItem, generateId(),
4522                        ImageUtils.buildOverlayBitmap(getApplicationContext(), null,
4523                                movieOverlay.getType(), movieOverlay.getTitle(),
4524                                movieOverlay.getSubtitle(), scaledWidth, scaledHeight),
4525                        movieOverlay.getStartTime(),
4526                        Math.min(movieOverlay.getDuration(),
4527                                mediaItem.getDuration() - movieOverlay.getStartTime()));
4528
4529                // Set the user attributes
4530                final Bundle userAttributes = movieOverlay.buildUserAttributes();
4531                for (String name : userAttributes.keySet()) {
4532                    if (MovieOverlay.getAttributeType(name).equals(Integer.class)) {
4533                        overlay.setUserAttribute(name,
4534                                Integer.toString(userAttributes.getInt(name)));
4535                    } else { // Strings
4536                        overlay.setUserAttribute(name, userAttributes.getString(name));
4537                    }
4538                }
4539                mediaItem.addOverlay(overlay);
4540            }
4541        }
4542    }
4543
4544    /**
4545     * Apply theme transitions after an item was removed
4546     *
4547     * @param videoEditor The video editor
4548     * @param themeId The theme id
4549     * @param removedItemPosition The position of the removed item
4550     * @param beginTransition The removed item begin transition
4551     * @param endTransition The removed item end transition
4552     *
4553     * @return The transition that was added
4554     */
4555    private Transition applyThemeAfterRemove(VideoEditor videoEditor, String themeId,
4556            int removedItemPosition, Transition beginTransition,
4557            Transition endTransition) throws IOException {
4558        final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
4559        final int mediaItemCount = mediaItems.size();
4560        if (mediaItemCount == 0) {
4561            return null;
4562        }
4563
4564        final Context context = getApplicationContext();
4565        final MovieTheme theme = MovieTheme.getTheme(context, themeId);
4566
4567        Transition transition = null;
4568        if (removedItemPosition == 0) { // First item removed
4569            if (theme.getBeginTransition() != null && beginTransition != null) {
4570                transition =
4571                    theme.getBeginTransition().buildTransition(context, null, mediaItems.get(0));
4572                videoEditor.addTransition(transition);
4573            }
4574        } else if (removedItemPosition == mediaItemCount) { // Last item removed
4575            if (theme.getEndTransition() != null && endTransition != null) {
4576                transition = theme.getEndTransition().buildTransition(context,
4577                            mediaItems.get(mediaItemCount - 1), null);
4578                videoEditor.addTransition(transition);
4579            }
4580        } else { // Mid item removed
4581            if (theme.getMidTransition() != null && beginTransition != null) {
4582                transition =
4583                    theme.getMidTransition().buildTransition(context,
4584                            mediaItems.get(removedItemPosition - 1),
4585                            mediaItems.get(removedItemPosition));
4586                videoEditor.addTransition(transition);
4587            }
4588        }
4589
4590        return transition;
4591    }
4592
4593    /**
4594     * Apply theme transitions after an item was moved
4595     *
4596     * @param videoEditor The video editor
4597     * @param themeId The theme id
4598     * @param movedMediaItem The moved media item
4599     * @param originalItemPosition The original media item position
4600     * @param beginTransition The moved item begin transition
4601     * @param endTransition The moved item end transition
4602     */
4603    private void applyThemeAfterMove(VideoEditor videoEditor, String themeId,
4604            MediaItem movedMediaItem, int originalItemPosition, Transition beginTransition,
4605            Transition endTransition) throws IOException {
4606        final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
4607        final int mediaItemCount = mediaItems.size();
4608        if (mediaItemCount == 0) {
4609            return;
4610        }
4611
4612        final Context context = getApplicationContext();
4613        final MovieTheme theme = MovieTheme.getTheme(context, themeId);
4614
4615        if (originalItemPosition == 0) { // First item moved
4616            if (theme.getBeginTransition() != null && beginTransition != null) {
4617                final Transition transition =
4618                    theme.getBeginTransition().buildTransition(context, null, mediaItems.get(0));
4619                videoEditor.addTransition(transition);
4620            }
4621        } else if (originalItemPosition == mediaItemCount - 1) { // Last item moved
4622            if (theme.getEndTransition() != null && endTransition != null) {
4623                final Transition transition = theme.getEndTransition().buildTransition(context,
4624                            mediaItems.get(mediaItemCount - 1), null);
4625                videoEditor.addTransition(transition);
4626            }
4627        } else { // Mid item moved
4628            final int newPosition = mediaItems.indexOf(movedMediaItem);
4629            if (newPosition > originalItemPosition) { // Moved forward
4630                if (endTransition != null && theme.getMidTransition() != null) {
4631                    final Transition transition = theme.getMidTransition().buildTransition(
4632                            context, mediaItems.get(originalItemPosition - 1),
4633                            mediaItems.get(originalItemPosition));
4634                    videoEditor.addTransition(transition);
4635                }
4636            } else { // Moved backward
4637                if (beginTransition != null && theme.getMidTransition() != null) {
4638                    final Transition transition = theme.getMidTransition().buildTransition(
4639                            context, mediaItems.get(originalItemPosition),
4640                                mediaItems.get(originalItemPosition + 1));
4641                    videoEditor.addTransition(transition);
4642                }
4643            }
4644        }
4645
4646        // Apply the theme at the new position
4647        applyThemeToMediaItem(videoEditor, themeId, movedMediaItem);
4648    }
4649
4650    /**
4651     * Copy the media items
4652     *
4653     * @param mediaItems The media items
4654     *
4655     * @return The list of media items
4656     */
4657    private List<MovieMediaItem> copyMediaItems(List<MediaItem> mediaItems) {
4658        final List<MovieMediaItem> movieMediaItems
4659            = new ArrayList<MovieMediaItem>(mediaItems.size());
4660        MovieMediaItem prevMediaItem = null;
4661        for (MediaItem mediaItem : mediaItems) {
4662            final MovieTransition prevTransition;
4663            if (prevMediaItem != null) {
4664                prevTransition = prevMediaItem.getEndTransition();
4665            } else if (mediaItem.getBeginTransition() != null) {
4666                prevTransition = new MovieTransition(mediaItem.getBeginTransition());
4667            } else {
4668                prevTransition = null;
4669            }
4670
4671            final MovieMediaItem movieMediaItem = new MovieMediaItem(mediaItem, prevTransition);
4672            movieMediaItems.add(movieMediaItem);
4673            prevMediaItem = movieMediaItem;
4674        }
4675
4676        return movieMediaItems;
4677    }
4678
4679    /**
4680     * Copy the audio tracks
4681     *
4682     * @param audioTracks The audio tracks
4683     *
4684     * @return The list of audio tracks
4685     */
4686    private List<MovieAudioTrack> copyAudioTracks(List<AudioTrack> audioTracks) {
4687        final List<MovieAudioTrack> movieAudioTracks
4688            = new ArrayList<MovieAudioTrack>(audioTracks.size());
4689        for (AudioTrack audioTrack : audioTracks) {
4690            movieAudioTracks.add(new MovieAudioTrack(audioTrack));
4691        }
4692        return movieAudioTracks;
4693    }
4694
4695    private static void logd(String message) {
4696        if (Log.isLoggable(TAG, Log.DEBUG)) {
4697            Log.d(TAG, message);
4698        }
4699    }
4700
4701    private static void logv(String message) {
4702        if (Log.isLoggable(TAG, Log.VERBOSE)) {
4703            Log.v(TAG, message);
4704        }
4705    }
4706
4707    /**
4708     * Worker thread that processes intents and maintains its own intent queue.
4709     */
4710    private class IntentProcessor extends Thread {
4711        private final BlockingQueue<Intent> mIntentQueue;
4712
4713        public IntentProcessor(String threadName) {
4714            super("IntentProcessor-" + threadName);
4715            mIntentQueue = new LinkedBlockingQueue<Intent>();
4716        }
4717
4718        @Override
4719        public void run() {
4720            try {
4721                while(true) {
4722                    processIntent(mIntentQueue.take());
4723                }
4724            } catch (InterruptedException e) {
4725                Log.e(TAG, "Terminating " + getName());
4726            }
4727        }
4728
4729        /**
4730         * Submits a new intent for processing.
4731         *
4732         * @param intent The intent to be processed
4733         */
4734        public void submit(Intent intent) {
4735            if (isAlive()) {
4736                mIntentQueue.add(intent);
4737            } else {
4738                Log.e(TAG, getName() + " should be started before submitting tasks.");
4739            }
4740        }
4741
4742        /**
4743         * Removes an intent from the queue.
4744         *
4745         * @param intent The intent to be removed
4746         *
4747         * @return true if the intent is removed
4748         */
4749        public boolean cancel(Intent intent) {
4750            return mIntentQueue.remove(intent);
4751        }
4752
4753        public Iterator<Intent> getIntentQueueIterator() {
4754            return mIntentQueue.iterator();
4755        }
4756
4757        public void quit() {
4758            // Display an error if the queue is not empty and clear it.
4759            final int queueSize = mIntentQueue.size();
4760            if (queueSize > 0) {
4761                Log.e(TAG, "Thread queue is not empty. Size: " + queueSize);
4762                mIntentQueue.clear();
4763            }
4764            interrupt();
4765        }
4766    }
4767}
4768