Home | History | Annotate | Download | only in service
      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 
     17 package com.android.videoeditor.service;
     18 
     19 import java.io.File;
     20 import java.io.FileInputStream;
     21 import java.io.FileNotFoundException;
     22 import java.io.FileOutputStream;
     23 import java.io.IOException;
     24 import java.io.StringWriter;
     25 import java.util.ArrayList;
     26 import java.util.List;
     27 
     28 import org.xmlpull.v1.XmlPullParser;
     29 import org.xmlpull.v1.XmlPullParserException;
     30 import org.xmlpull.v1.XmlSerializer;
     31 
     32 import android.media.videoeditor.MediaProperties;
     33 import android.media.videoeditor.MediaVideoItem;
     34 import android.media.videoeditor.VideoEditor;
     35 import android.media.videoeditor.VideoEditor.PreviewProgressListener;
     36 import android.net.Uri;
     37 import android.util.Xml;
     38 import android.view.SurfaceHolder;
     39 
     40 
     41 /**
     42  * The video editor project encapsulates the video editor and the project metadata.
     43  */
     44 public class VideoEditorProject {
     45     // The name of the metadata file
     46     private final static String PROJECT_METADATA_FILENAME = "metadata.xml";
     47 
     48     public static final int DEFAULT_ZOOM_LEVEL = 20;
     49 
     50     // XML definitions
     51     private static final String TAG_PROJECT = "project";
     52     private static final String TAG_MOVIE = "movie";
     53     private static final String TAG_DOWNLOAD = "download";
     54     private static final String ATTR_NAME = "name";
     55     private static final String ATTR_URI = "uri";
     56     private static final String ATTR_SAVED = "saved";
     57     private static final String ATTR_THEME = "theme";
     58     private static final String ATTR_PLAYHEAD_POSITION = "playhead";
     59     private static final String ATTR_DURATION = "duration";
     60     private static final String ATTR_ZOOM_LEVEL = "zoom_level";
     61     private static final String ATTR_MIME = "mime";
     62     private static final String ATTR_FILENAME = "filename";
     63     private static final String ATTR_TIME = "time";
     64 
     65     // Instance variables
     66     private final VideoEditor mVideoEditor;
     67     private final String mProjectPath;
     68     private final long mProjectDurationMs;
     69     private final List<Download> mDownloads;
     70     private String mProjectName;
     71     private long mLastSaved;
     72     private Uri mExportedMovieUri;
     73     private int mAspectRatio;
     74     private String mTheme;
     75     private long mPlayheadPosMs;
     76     private int mZoomLevel;
     77     private List<MovieMediaItem> mMediaItems = new ArrayList<MovieMediaItem>();
     78     private List<MovieAudioTrack> mAudioTracks = new ArrayList<MovieAudioTrack>();
     79     private boolean mClean;
     80 
     81     /**
     82      * Download item
     83      */
     84     public static class Download {
     85         private final String mMediaUri;
     86         private final String mMimeType;
     87         private final String mFilename;
     88         private final long mTime;
     89 
     90         /**
     91          * Constructor
     92          *
     93          * @param mediaUri The media URI
     94          * @param mimeType The mime type
     95          * @param filename The filename
     96          * @param time The time when the file was downloaded
     97          */
     98         private Download(String mediaUri, String mimeType, String filename, long time) {
     99             mMediaUri = mediaUri;
    100             mMimeType = mimeType;
    101             mFilename = filename;
    102             mTime = time;
    103         }
    104 
    105         /**
    106          * @return the media URI
    107          */
    108         public String getMediaUri() {
    109             return mMediaUri;
    110         }
    111 
    112         /**
    113          * @return the mime type
    114          */
    115         public String getMimeType() {
    116             return mMimeType;
    117         }
    118 
    119         /**
    120          * @return the filename
    121          */
    122         public String getFilename() {
    123             return mFilename;
    124         }
    125 
    126         /**
    127          * @return the mTime
    128          */
    129         public long getTime() {
    130             return mTime;
    131         }
    132     }
    133 
    134     /**
    135      * Constructor
    136      *
    137      * @param videoEditor The video editor. Note that this can be null when
    138      *  we create the project for the purpose of displaying a project preview.
    139      * @param projectPath The project path
    140      * @param projectName The project name
    141      * @param lastSaved Time when project was last saved
    142      * @param playheadPosMs The playhead position
    143      * @param durationMs The project duration
    144      * @param zoomLevel The zoom level
    145      * @param exportedMovieUri The exported movie URI
    146      * @param theme The project theme
    147      * @param downloads The list of downloads
    148      */
    149     VideoEditorProject(VideoEditor videoEditor, String projectPath, String projectName,
    150             long lastSaved, long playheadPosMs, long durationMs, int zoomLevel,
    151             Uri exportedMovieUri, String theme, List<Download> downloads) {
    152         mVideoEditor = videoEditor;
    153         if (videoEditor != null) {
    154             mAspectRatio = videoEditor.getAspectRatio();
    155         }
    156 
    157         if (downloads != null) {
    158             mDownloads = downloads;
    159         } else {
    160             mDownloads = new ArrayList<Download>();
    161         }
    162         mProjectPath = projectPath;
    163         mProjectName = projectName;
    164         mLastSaved = lastSaved;
    165         mPlayheadPosMs = playheadPosMs;
    166         mProjectDurationMs = durationMs;
    167         mZoomLevel = zoomLevel;
    168         mExportedMovieUri = exportedMovieUri;
    169         mTheme = theme;
    170         mClean = true;
    171     }
    172 
    173     /**
    174      * @param clean true if this is clean
    175      */
    176     public void setClean(boolean clean) {
    177         mClean = clean;
    178     }
    179 
    180     /**
    181      * @return true if no change was made
    182      */
    183     public boolean isClean() {
    184         return mClean;
    185     }
    186 
    187     /**
    188      * @return The project path
    189      */
    190     public String getPath() {
    191         return mProjectPath;
    192     }
    193 
    194     /**
    195      * @param projectName The project name
    196      */
    197     public void setProjectName(String projectName) {
    198         mProjectName = projectName;
    199         mClean = false;
    200     }
    201 
    202     /**
    203      * @return The project name
    204      */
    205     public String getName() {
    206         return mProjectName;
    207     }
    208 
    209     /**
    210      * @return Time when time was last saved
    211      */
    212     public long getLastSaved() {
    213         return mLastSaved;
    214     }
    215 
    216     /**
    217      * @return The project duration.
    218      *
    219      * Note: This method should only be called to retrieve the project duration
    220      * as saved on disk. Once a project is opened call computeDuration() to get
    221      * the current duration.
    222      */
    223     public long getProjectDuration() {
    224         return mProjectDurationMs;
    225     }
    226 
    227     /**
    228      * @return The zoom level
    229      */
    230     public int getZoomLevel() {
    231         return mZoomLevel;
    232     }
    233 
    234     /**
    235      * @param zoomLevel The zoom level
    236      */
    237     public void setZoomLevel(int zoomLevel) {
    238         mZoomLevel = zoomLevel;
    239     }
    240 
    241     /**
    242      * @return The aspect ratio
    243      */
    244     public int getAspectRatio() {
    245         return mAspectRatio;
    246     }
    247 
    248     /**
    249      * @return The playhead position
    250      */
    251     public long getPlayheadPos() {
    252         return mPlayheadPosMs;
    253     }
    254 
    255     /**
    256      * @param playheadPosMs The playhead position
    257      */
    258     public void setPlayheadPos(long playheadPosMs) {
    259         mPlayheadPosMs = playheadPosMs;
    260     }
    261 
    262     /**
    263      * @param aspectRatio The aspect ratio
    264      */
    265     void setAspectRatio(int aspectRatio) {
    266         mAspectRatio = aspectRatio;
    267         mClean = false;
    268     }
    269 
    270     /**
    271      * Add the URI of an exported movie
    272      *
    273      * @param uri The movie URI
    274      */
    275     void addExportedMovieUri(Uri uri) {
    276         mExportedMovieUri = uri;
    277         mClean = false;
    278     }
    279 
    280     /**
    281      * @return The exported movie URI
    282      */
    283     public Uri getExportedMovieUri() {
    284         return mExportedMovieUri;
    285     }
    286 
    287     /**
    288      * @param theme The theme
    289      */
    290     void setTheme(String theme) {
    291         mTheme = theme;
    292         mClean = false;
    293     }
    294 
    295     /**
    296      * @return The theme
    297      */
    298     public String getTheme() {
    299         return mTheme;
    300     }
    301 
    302     /**
    303      * Set the media items
    304      *
    305      * @param mediaItems The media items
    306      */
    307     void setMediaItems(List<MovieMediaItem> mediaItems) {
    308         mMediaItems = mediaItems;
    309         mClean = false;
    310     }
    311 
    312     /**
    313      * Insert a media item after the specified media item id
    314      *
    315      * @param mediaItem The media item
    316      * @param afterMediaItemId Insert after this media item id
    317      */
    318     void insertMediaItem(MovieMediaItem mediaItem, String afterMediaItemId) {
    319         if (afterMediaItemId == null) {
    320             if (mMediaItems.size() > 0) {
    321                 // Invalidate the transition at the beginning of the timeline
    322                 final MovieMediaItem firstMediaItem = mMediaItems.get(0);
    323                 if (firstMediaItem.getBeginTransition() != null) {
    324                     firstMediaItem.setBeginTransition(null);
    325                 }
    326             }
    327 
    328             mMediaItems.add(0, mediaItem);
    329             mClean = false;
    330         } else {
    331             final int mediaItemCount = mMediaItems.size();
    332             for (int i = 0; i < mediaItemCount; i++) {
    333                 final MovieMediaItem mi = mMediaItems.get(i);
    334                 if (mi.getId().equals(afterMediaItemId)) {
    335                     // Invalidate the transition at the end of this media item
    336                     mi.setEndTransition(null);
    337                     // Invalidate the reference in the next media item (if any)
    338                     if (i < mediaItemCount - 1) {
    339                         mMediaItems.get(i + 1).setBeginTransition(null);
    340                     }
    341 
    342                     // Insert the new media item
    343                     mMediaItems.add(i + 1, mediaItem);
    344                     mClean = false;
    345                     return;
    346                 }
    347             }
    348 
    349             throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId);
    350         }
    351     }
    352 
    353     /**
    354      * Update the specified media item
    355      *
    356      * @param newMediaItem The media item can be a new instance of the media
    357      *      item or an updated version of the same instance.
    358      */
    359     void updateMediaItem(MovieMediaItem newMediaItem) {
    360         final String newMediaItemId = newMediaItem.getId();
    361         final int count = mMediaItems.size();
    362         for (int i = 0; i < count; i++) {
    363             final MovieMediaItem mediaItem = mMediaItems.get(i);
    364             if (mediaItem.getId().equals(newMediaItemId)) {
    365                 mMediaItems.set(i, newMediaItem);
    366                 mClean = false;
    367                 // Update the transitions of the previous and next item
    368                 if (i > 0) {
    369                     final MovieMediaItem prevMediaItem = mMediaItems.get(i - 1);
    370                     prevMediaItem.setEndTransition(newMediaItem.getBeginTransition());
    371                 }
    372 
    373                 if (i < count - 1) {
    374                     final MovieMediaItem nextMediaItem = mMediaItems.get(i + 1);
    375                     nextMediaItem.setBeginTransition(newMediaItem.getEndTransition());
    376                 }
    377                 break;
    378             }
    379         }
    380     }
    381 
    382     /**
    383      * Remove the specified media item
    384      *
    385      * @param mediaItemId The media item id
    386      * @param transition The transition to be set between at the delete
    387      *      position
    388      */
    389     void removeMediaItem(String mediaItemId, MovieTransition transition) {
    390         String prevMediaItemId = null;
    391         final int count = mMediaItems.size();
    392         for (int i = 0; i < count; i++) {
    393             final MovieMediaItem mediaItem = mMediaItems.get(i);
    394             if (mediaItem.getId().equals(mediaItemId)) {
    395                 mMediaItems.remove(i);
    396                 mClean = false;
    397                 if (transition != null) {
    398                     addTransition(transition, prevMediaItemId);
    399                 } else {
    400                     if (i > 0) {
    401                         final MovieMediaItem prevMediaItem = mMediaItems.get(i - 1);
    402                         prevMediaItem.setEndTransition(null);
    403                     }
    404 
    405                     if (i < count - 1) {
    406                         final MovieMediaItem nextMediaItem = mMediaItems.get(i);
    407                         nextMediaItem.setBeginTransition(null);
    408                     }
    409                 }
    410                 break;
    411             }
    412 
    413             prevMediaItemId = mediaItem.getId();
    414         }
    415     }
    416 
    417     /**
    418      * @return The media items list
    419      */
    420     public List<MovieMediaItem> getMediaItems() {
    421         return mMediaItems;
    422     }
    423 
    424     /**
    425      * @return The media item count
    426      */
    427     public int getMediaItemCount() {
    428         return mMediaItems.size();
    429     }
    430 
    431     /**
    432      * @param mediaItemId The media item id
    433      *
    434      * @return The media item
    435      */
    436     public MovieMediaItem getMediaItem(String mediaItemId) {
    437         for (MovieMediaItem mediaItem : mMediaItems) {
    438             if (mediaItem.getId().equals(mediaItemId)) {
    439                 return mediaItem;
    440             }
    441         }
    442 
    443         return null;
    444     }
    445 
    446     /**
    447      * @return The first media item
    448      */
    449     public MovieMediaItem getFirstMediaItem() {
    450         if (mMediaItems.size() == 0) {
    451             return null;
    452         } else {
    453             return mMediaItems.get(0);
    454         }
    455     }
    456 
    457     /**
    458      * Check if the specified media item id is the first media item
    459      *
    460      * @param mediaItemId The media item id
    461      *
    462      * @return true if this is the first media item
    463      */
    464     public boolean isFirstMediaItem(String mediaItemId) {
    465         final MovieMediaItem mediaItem = getFirstMediaItem();
    466         if (mediaItem == null) {
    467             return false;
    468         } else {
    469             return mediaItem.getId().equals(mediaItemId);
    470         }
    471     }
    472 
    473     /**
    474      * @return The last media item. {@code null} if no item is in the project.
    475      */
    476     public MovieMediaItem getLastMediaItem() {
    477         final int count = mMediaItems.size();
    478         if (count == 0) {
    479             return null;
    480         } else {
    481             return mMediaItems.get(count - 1);
    482         }
    483     }
    484 
    485     /**
    486      * Gets the id of the last media item in this project.
    487      *
    488      * @return Id of the last media item. {@code null} if no item is in this project.
    489      */
    490     public String getLastMediaItemId() {
    491         MovieMediaItem lastMediaItem = getLastMediaItem();
    492         if (lastMediaItem != null)
    493             return lastMediaItem.getId();
    494         return null;
    495     }
    496 
    497     /**
    498      * Check if the specified media item id is the last media item
    499      *
    500      * @param mediaItemId The media item id
    501      *
    502      * @return true if this is the last media item
    503      */
    504     public boolean isLastMediaItem(String mediaItemId) {
    505         final MovieMediaItem mediaItem = getLastMediaItem();
    506         if (mediaItem == null) {
    507             return false;
    508         } else {
    509             return mediaItem.getId().equals(mediaItemId);
    510         }
    511     }
    512 
    513     /**
    514      * Find the previous media item with the specified id
    515      *
    516      * @param mediaItemId The media item id
    517      * @return The previous media item
    518      */
    519     public MovieMediaItem getPreviousMediaItem(String mediaItemId) {
    520         MovieMediaItem prevMediaItem = null;
    521         for (MovieMediaItem mediaItem : mMediaItems) {
    522             if (mediaItemId.equals(mediaItem.getId())) {
    523                 break;
    524             } else {
    525                 prevMediaItem = mediaItem;
    526             }
    527         }
    528 
    529         return prevMediaItem;
    530     }
    531 
    532     /**
    533      * Find the next media item with the specified id
    534      *
    535      * @param mediaItemId The media item id
    536      * @return The next media item
    537      */
    538     public MovieMediaItem getNextMediaItem(String mediaItemId) {
    539         boolean getNext = false;
    540         final int count = mMediaItems.size();
    541         for (int i = 0; i < count; i++) {
    542             final MovieMediaItem mi = mMediaItems.get(i);
    543             if (getNext) {
    544                 return mi;
    545             } else {
    546                 if (mediaItemId.equals(mi.getId())) {
    547                     getNext = true;
    548                 }
    549             }
    550         }
    551 
    552         return null;
    553     }
    554 
    555     /**
    556      * Get the previous media item
    557      *
    558      * @param positionMs The current position in ms
    559      * @return The previous media item
    560      */
    561     public MovieMediaItem getPreviousMediaItem(long positionMs) {
    562         long startTimeMs = 0;
    563         MovieMediaItem prevMediaItem = null;
    564         for (MovieMediaItem mediaItem : mMediaItems) {
    565             if (positionMs == startTimeMs) {
    566                 break;
    567             } else if (positionMs > startTimeMs
    568                     && positionMs < startTimeMs + mediaItem.getAppTimelineDuration()) {
    569                 return mediaItem;
    570             } else {
    571                 prevMediaItem = mediaItem;
    572             }
    573 
    574             startTimeMs += mediaItem.getAppTimelineDuration();
    575             if (mediaItem.getEndTransition() != null) {
    576                 startTimeMs -= mediaItem.getEndTransition().getAppDuration();
    577             }
    578         }
    579 
    580         return prevMediaItem;
    581     }
    582 
    583     /**
    584      * Get the next media item
    585      *
    586      * @param positionMs The current position in ms
    587      * @return The next media item
    588      */
    589     public MovieMediaItem getNextMediaItem(long positionMs) {
    590         long startTimeMs = 0;
    591         final int count = mMediaItems.size();
    592         for (int i = 0; i < count; i++) {
    593             final MovieMediaItem mediaItem = mMediaItems.get(i);
    594             if (positionMs >= startTimeMs
    595                     && positionMs < startTimeMs + mediaItem.getAppTimelineDuration() -
    596                     getEndTransitionDuration(mediaItem)) {
    597                 if (i < count - 1) {
    598                     return mMediaItems.get(i + 1);
    599                 } else {
    600                     return null;
    601                 }
    602             } else if (positionMs >= startTimeMs
    603                     && positionMs < startTimeMs + mediaItem.getAppTimelineDuration()) {
    604                 if (i < count - 2) {
    605                     return mMediaItems.get(i + 2);
    606                 } else {
    607                     return null;
    608                 }
    609             } else {
    610                 startTimeMs += mediaItem.getAppTimelineDuration();
    611                 startTimeMs -= getEndTransitionDuration(mediaItem);
    612             }
    613         }
    614 
    615         return null;
    616     }
    617 
    618     /**
    619      * Get the beginning media item of the specified transition
    620      *
    621      * @param transition The transition
    622      *
    623      * @return The media item
    624      */
    625     public MovieMediaItem getPreviousMediaItem(MovieTransition transition) {
    626         final int count = mMediaItems.size();
    627         for (int i = 0; i < count; i++) {
    628             final MovieMediaItem mediaItem = mMediaItems.get(i);
    629             if (i == 0) {
    630                 if (mediaItem.getBeginTransition() == transition) {
    631                     return null;
    632                 }
    633             }
    634 
    635             if (mediaItem.getEndTransition() == transition) {
    636                 return mediaItem;
    637             }
    638         }
    639 
    640         return null;
    641     }
    642 
    643     /**
    644      * Return the end transition duration
    645      *
    646      * @param mediaItem The media item
    647      * @return the end transition duration
    648      */
    649     private static long getEndTransitionDuration(MovieMediaItem mediaItem) {
    650         if (mediaItem.getEndTransition() != null) {
    651             return mediaItem.getEndTransition().getAppDuration();
    652         } else {
    653             return 0;
    654         }
    655     }
    656 
    657     /**
    658      * Determine the media item after which a new media item will be inserted.
    659      *
    660      * @param timeMs The inquiry position
    661      *
    662      * @return The media item after which the insertion will be performed
    663      */
    664     public MovieMediaItem getInsertAfterMediaItem(long timeMs) {
    665         long beginMs = 0;
    666         long endMs = 0;
    667         MovieMediaItem prevMediaItem = null;
    668         final int mediaItemsCount = mMediaItems.size();
    669         for (int i = 0; i < mediaItemsCount; i++) {
    670             final MovieMediaItem mediaItem = mMediaItems.get(i);
    671 
    672             endMs = beginMs + mediaItem.getAppTimelineDuration();
    673 
    674             if (mediaItem.getEndTransition() != null) {
    675                 if (i < mediaItemsCount - 1) {
    676                     endMs -= mediaItem.getEndTransition().getAppDuration();
    677                 }
    678             }
    679 
    680             if (timeMs >= beginMs && timeMs <= endMs) {
    681                 if (timeMs - beginMs < endMs - timeMs) { // Closer to the beginning
    682                     return prevMediaItem;
    683                 } else { // Closer to the end
    684                     return mediaItem; // Insert after this item
    685                 }
    686             }
    687 
    688             beginMs = endMs;
    689             prevMediaItem = mediaItem;
    690         }
    691 
    692         return null;
    693     }
    694 
    695     /**
    696      * @return true if media items have different aspect ratios
    697      */
    698     public boolean hasMultipleAspectRatios() {
    699         int aspectRatio = MediaProperties.ASPECT_RATIO_UNDEFINED;
    700         for (MovieMediaItem mediaItem : mMediaItems) {
    701             if (aspectRatio == MediaProperties.ASPECT_RATIO_UNDEFINED) {
    702                 aspectRatio = mediaItem.getAspectRatio();
    703             } else if (mediaItem.getAspectRatio() != aspectRatio) {
    704                 return true;
    705             }
    706         }
    707 
    708         return false;
    709     }
    710 
    711     /**
    712      * @return The list of unique aspect ratios
    713      */
    714     public ArrayList<Integer> getUniqueAspectRatiosList() {
    715         final ArrayList<Integer> aspectRatiosList = new ArrayList<Integer>();
    716         for (MovieMediaItem mediaItem : mMediaItems) {
    717             int aspectRatio = mediaItem.getAspectRatio();
    718             if (!aspectRatiosList.contains(aspectRatio)) {
    719                 aspectRatiosList.add(aspectRatio);
    720             }
    721         }
    722 
    723         return aspectRatiosList;
    724     }
    725 
    726     /**
    727      * Add a new transition
    728      *
    729      * @param transition The transition
    730      * @param afterMediaItemId Add the transition after this media item
    731      */
    732     void addTransition(MovieTransition transition, String afterMediaItemId) {
    733         final int count = mMediaItems.size();
    734         if (afterMediaItemId != null) {
    735             MovieMediaItem afterMediaItem = null;
    736             int afterMediaItemIndex = -1;
    737             for (int i = 0; i < count; i++) {
    738                 final MovieMediaItem mediaItem = mMediaItems.get(i);
    739                 if (mediaItem.getId().equals(afterMediaItemId)) {
    740                     afterMediaItem = mediaItem;
    741                     afterMediaItemIndex = i;
    742                     break;
    743                 }
    744             }
    745 
    746             // Link the transition to the next and previous media items
    747             if (afterMediaItem == null) {
    748                 throw new IllegalArgumentException("Media item not found: " + afterMediaItemId);
    749             }
    750 
    751             afterMediaItem.setEndTransition(transition);
    752 
    753             if (afterMediaItemIndex < count - 1) {
    754                 final MovieMediaItem beforeMediaItem = mMediaItems.get(afterMediaItemIndex + 1);
    755                 beforeMediaItem.setBeginTransition(transition);
    756             }
    757         } else {
    758             if (count == 0) {
    759                 throw new IllegalArgumentException("Media item not found at the beginning");
    760             }
    761 
    762             final MovieMediaItem beforeMediaItem = mMediaItems.get(0);
    763             beforeMediaItem.setBeginTransition(transition);
    764         }
    765 
    766         mClean = false;
    767     }
    768 
    769     /**
    770      * Remove the specified transition
    771      *
    772      * @param transitionId The transition id
    773      */
    774     void removeTransition(String transitionId) {
    775         final int count = mMediaItems.size();
    776         for (int i = 0; i < count; i++) {
    777             final MovieMediaItem mediaItem = mMediaItems.get(i);
    778             final MovieTransition beginTransition = mediaItem.getBeginTransition();
    779             if (beginTransition != null && beginTransition.getId().equals(transitionId)) {
    780                 mediaItem.setBeginTransition(null);
    781                 break;
    782             }
    783 
    784             final MovieTransition endTransition = mediaItem.getEndTransition();
    785             if (endTransition != null && endTransition.getId().equals(transitionId)) {
    786                 mediaItem.setEndTransition(null);
    787             }
    788         }
    789 
    790         mClean = false;
    791     }
    792 
    793     /**
    794      * Find the transition with the specified id
    795      *
    796      * @param transitionId The transition id
    797      * @return The transition
    798      */
    799     public MovieTransition getTransition(String transitionId) {
    800         final MovieMediaItem firstMediaItem = getFirstMediaItem();
    801         if (firstMediaItem == null) {
    802             return null;
    803         }
    804 
    805         final MovieTransition beginTransition = firstMediaItem.getBeginTransition();
    806         if (beginTransition != null && beginTransition.getId().equals(transitionId)) {
    807             return beginTransition;
    808         }
    809 
    810         for (MovieMediaItem mediaItem : mMediaItems) {
    811             final MovieTransition endTransition = mediaItem.getEndTransition();
    812             if (endTransition != null && endTransition.getId().equals(transitionId)) {
    813                 return endTransition;
    814             }
    815         }
    816 
    817         return null;
    818     }
    819 
    820     /**
    821      * Add the overlay
    822      *
    823      * @param mediaItemId The media item id
    824      * @param overlay The overlay
    825      */
    826     void addOverlay(String mediaItemId, MovieOverlay overlay) {
    827         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
    828 
    829         // Remove an existing overlay (if any)
    830         final MovieOverlay oldOverlay = mediaItem.getOverlay();
    831         if (oldOverlay != null) {
    832             mediaItem.removeOverlay(oldOverlay.getId());
    833         }
    834 
    835         mediaItem.addOverlay(overlay);
    836         mClean = false;
    837     }
    838 
    839     /**
    840      * Remove the specified overlay
    841      *
    842      * @param mediaItemId The media item id
    843      * @param overlayId The overlay id
    844      */
    845     void removeOverlay(String mediaItemId, String overlayId) {
    846         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
    847         mediaItem.removeOverlay(overlayId);
    848         mClean = false;
    849     }
    850 
    851     /**
    852      * Get the specified overlay
    853      *
    854      * @param mediaItemId The media item id
    855      * @param overlayId The overlay id
    856      * @return The movie overlay
    857      */
    858     public MovieOverlay getOverlay(String mediaItemId, String overlayId) {
    859         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
    860         return mediaItem.getOverlay();
    861     }
    862 
    863     /**
    864      * Add the effect
    865      *
    866      * @param mediaItemId The media item id
    867      * @param effect The effect
    868      */
    869     void addEffect(String mediaItemId, MovieEffect effect) {
    870         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
    871         // Remove an existing effect
    872         final MovieEffect oldEffect = mediaItem.getEffect();
    873         if (oldEffect != null) {
    874             mediaItem.removeEffect(oldEffect.getId());
    875         }
    876 
    877         mediaItem.addEffect(effect);
    878         mClean = false;
    879     }
    880 
    881     /**
    882      * Remove the specified effect
    883      *
    884      * @param mediaItemId The media item id
    885      * @param effectId The effect id
    886      */
    887     void removeEffect(String mediaItemId, String effectId) {
    888         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
    889         mediaItem.removeEffect(effectId);
    890         mClean = false;
    891     }
    892 
    893     /**
    894      * Get the specified effect
    895      *
    896      * @param mediaItemId The media item id
    897      * @param effectId The effect id
    898      * @return The movie effect
    899      */
    900     public MovieEffect getEffect(String mediaItemId, String effectId) {
    901         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
    902         return mediaItem.getEffect();
    903     }
    904 
    905     /**
    906      * Set the audio tracks
    907      *
    908      * @param audioTracks The audio tracks
    909      */
    910     void setAudioTracks(List<MovieAudioTrack> audioTracks) {
    911         mAudioTracks = audioTracks;
    912         mClean = false;
    913     }
    914 
    915     /**
    916      * Add an audio track
    917      *
    918      * @param audioTrack The audio track
    919      */
    920     void addAudioTrack(MovieAudioTrack audioTrack) {
    921         mAudioTracks.add(audioTrack);
    922         mClean = false;
    923     }
    924 
    925     /**
    926      * Remove the specified audio track
    927      *
    928      * @param audioTrackId The audio track id
    929      */
    930     void removeAudioTrack(String audioTrackId) {
    931         final int count = mAudioTracks.size();
    932         for (int i = 0; i < count; i++) {
    933             final MovieAudioTrack audioTrack = mAudioTracks.get(i);
    934             if (audioTrack.getId().equals(audioTrackId)) {
    935                 mAudioTracks.remove(i);
    936                 mClean = false;
    937                 break;
    938             }
    939         }
    940     }
    941 
    942     /**
    943      * @return The audio tracks
    944      */
    945     public List<MovieAudioTrack> getAudioTracks() {
    946         return mAudioTracks;
    947     }
    948 
    949     /**
    950      * @param audioTrackId The audio track id
    951      * @return The audio track
    952      */
    953     public MovieAudioTrack getAudioTrack(String audioTrackId) {
    954         for (MovieAudioTrack audioTrack : mAudioTracks) {
    955             if (audioTrack.getId().equals(audioTrackId)) {
    956                 return audioTrack;
    957             }
    958         }
    959 
    960         return null;
    961     }
    962 
    963     /**
    964      * Compute the begin time for this media item
    965      *
    966      * @param mediaItemId The media item id for which we compute the begin time
    967      *
    968      * @return The begin time for this media item
    969      */
    970     public long getMediaItemBeginTime(String mediaItemId) {
    971         long beginMs = 0;
    972         final int mediaItemsCount = mMediaItems.size();
    973         for (int i = 0; i < mediaItemsCount; i++) {
    974             final MovieMediaItem mi = mMediaItems.get(i);
    975             if (mi.getId().equals(mediaItemId)) {
    976                 break;
    977             }
    978 
    979             beginMs += mi.getAppTimelineDuration();
    980 
    981             if (mi.getEndTransition() != null) {
    982                 if (i < mediaItemsCount - 1) {
    983                     beginMs -= mi.getEndTransition().getAppDuration();
    984                 }
    985             }
    986         }
    987 
    988         return beginMs;
    989     }
    990 
    991     /**
    992      * @return The total duration
    993      */
    994     public long computeDuration() {
    995         long totalDurationMs = 0;
    996         final int mediaItemsCount = mMediaItems.size();
    997         for (int i = 0; i < mediaItemsCount; i++) {
    998             final MovieMediaItem mediaItem = mMediaItems.get(i);
    999             totalDurationMs += mediaItem.getAppTimelineDuration();
   1000 
   1001             if (mediaItem.getEndTransition() != null) {
   1002                 if (i < mediaItemsCount - 1) {
   1003                     totalDurationMs -= mediaItem.getEndTransition().getAppDuration();
   1004                 }
   1005             }
   1006         }
   1007 
   1008         return totalDurationMs;
   1009     }
   1010 
   1011     /**
   1012      * Render a frame according to the preview aspect ratio and activating all
   1013      * storyboard items relative to the specified time.
   1014      *
   1015      * @param surfaceHolder SurfaceHolder used by the application
   1016      * @param timeMs time corresponding to the frame to display
   1017      * @param overlayData The overlay data
   1018      *
   1019      * @return The accurate time stamp of the frame that is rendered.
   1020      * @throws IllegalStateException if a preview or an export is already in
   1021      *             progress
   1022      * @throws IllegalArgumentException if time is negative or beyond the
   1023      *             preview duration
   1024      */
   1025     public long renderPreviewFrame(SurfaceHolder surfaceHolder, long timeMs,
   1026             VideoEditor.OverlayData overlayData) {
   1027         if (mVideoEditor != null) {
   1028             return mVideoEditor.renderPreviewFrame(surfaceHolder, timeMs, overlayData);
   1029         } else {
   1030             return 0;
   1031         }
   1032     }
   1033 
   1034     /**
   1035      * Render a frame of a media item.
   1036      *
   1037      * @param surfaceHolder SurfaceHolder used by the application
   1038      * @param mediaItemId The media item id
   1039      * @param timeMs time corresponding to the frame to display
   1040      *
   1041      * @return The accurate time stamp of the frame that is rendered .
   1042      * @throws IllegalStateException if a preview or an export is already in
   1043      *             progress
   1044      * @throws IllegalArgumentException if time is negative or beyond the
   1045      *             preview duration
   1046      */
   1047     public long renderMediaItemFrame(SurfaceHolder surfaceHolder, String mediaItemId,
   1048             long timeMs) {
   1049         if (mVideoEditor != null) {
   1050             final MediaVideoItem mediaItem =
   1051                 (MediaVideoItem)mVideoEditor.getMediaItem(mediaItemId);
   1052             if (mediaItem != null) {
   1053                 return mediaItem.renderFrame(surfaceHolder, timeMs);
   1054             } else {
   1055                 return -1;
   1056             }
   1057         } else {
   1058             return 0;
   1059         }
   1060     }
   1061 
   1062     /**
   1063      * Start the preview of all the storyboard items applied on all MediaItems
   1064      * This method does not block (does not wait for the preview to complete).
   1065      * The PreviewProgressListener allows to track the progress at the time
   1066      * interval determined by the callbackAfterFrameCount parameter. The
   1067      * SurfaceHolder has to be created and ready for use before calling this
   1068      * method. The method is a no-op if there are no MediaItems in the
   1069      * storyboard.
   1070      *
   1071      * @param surfaceHolder SurfaceHolder where the preview is rendered.
   1072      * @param fromMs The time (relative to the timeline) at which the preview
   1073      *            will start
   1074      * @param toMs The time (relative to the timeline) at which the preview will
   1075      *            stop. Use -1 to play to the end of the timeline
   1076      * @param loop true if the preview should be looped once it reaches the end
   1077      * @param callbackAfterFrameCount The listener interface should be invoked
   1078      *            after the number of frames specified by this parameter.
   1079      * @param listener The listener which will be notified of the preview
   1080      *            progress
   1081      *
   1082      * @throws IllegalArgumentException if fromMs is beyond the preview duration
   1083      * @throws IllegalStateException if a preview or an export is already in
   1084      *             progress
   1085      */
   1086     public void startPreview(SurfaceHolder surfaceHolder, long fromMs, long toMs, boolean loop,
   1087             int callbackAfterFrameCount, PreviewProgressListener listener) {
   1088         if (mVideoEditor != null) {
   1089             mVideoEditor.startPreview(surfaceHolder, fromMs, toMs, loop, callbackAfterFrameCount,
   1090                     listener);
   1091         }
   1092     }
   1093 
   1094     /**
   1095      * Stop the current preview. This method blocks until ongoing preview is
   1096      * stopped. Ignored if there is no preview running.
   1097      *
   1098      * @return The accurate current time when stop is effective expressed in
   1099      *         milliseconds
   1100      */
   1101     public long stopPreview() {
   1102         if (mVideoEditor != null) {
   1103             return mVideoEditor.stopPreview();
   1104         } else {
   1105             return 0;
   1106         }
   1107     }
   1108 
   1109     /**
   1110      * Clear the surface
   1111      *
   1112      * @param surfaceHolder SurfaceHolder where the preview is rendered.
   1113      */
   1114     public void clearSurface(SurfaceHolder surfaceHolder) {
   1115         if (mVideoEditor != null) {
   1116             mVideoEditor.clearSurface(surfaceHolder);
   1117         }
   1118     }
   1119 
   1120     /**
   1121      * Release the project
   1122      */
   1123     public void release() {
   1124     }
   1125 
   1126     /**
   1127      * Add a new download to the project
   1128      *
   1129      * @param mediaUri The media URI
   1130      * @param mimeType The mime type
   1131      * @param filename The local filename
   1132      */
   1133     public void addDownload(String mediaUri, String mimeType, String filename) {
   1134         mDownloads.add(new Download(mediaUri, mimeType, filename, System.currentTimeMillis()));
   1135         mClean = false;
   1136     }
   1137 
   1138     /**
   1139      * Remove a download
   1140      *
   1141      * @param mediaUri The media URI
   1142      */
   1143     public void removeDownload(String mediaUri) {
   1144         final int count = mDownloads.size();
   1145         for (int i = 0; i < count; i++) {
   1146             final Download download = mDownloads.get(i);
   1147             final String uri = download.getMediaUri();
   1148             if (mediaUri.equals(uri)) {
   1149                 // Delete the file associated with the download
   1150                 final String filename = download.getFilename();
   1151                 new File(filename).delete();
   1152 
   1153                 // Remove the download from the list
   1154                 mDownloads.remove(i);
   1155                 mClean = false;
   1156                 break;
   1157             }
   1158         }
   1159     }
   1160 
   1161     /**
   1162      * @return The list of downloads
   1163      */
   1164     public List<Download> getDownloads() {
   1165         return mDownloads;
   1166     }
   1167 
   1168     /**
   1169      * Load metadata from file
   1170      *
   1171      * @param videoEditor The video editor
   1172      * @param projectPath The project path
   1173      *
   1174      * @return A new instance of the VideoEditorProject
   1175      */
   1176     public static VideoEditorProject fromXml(VideoEditor videoEditor, String projectPath)
   1177             throws XmlPullParserException, FileNotFoundException, IOException {
   1178         final File file = new File(projectPath, PROJECT_METADATA_FILENAME);
   1179         final FileInputStream fis = new FileInputStream(file);
   1180         final List<Download> downloads = new ArrayList<Download>();
   1181         try {
   1182             // Load the metadata
   1183             final XmlPullParser parser = Xml.newPullParser();
   1184             parser.setInput(fis, "UTF-8");
   1185             int eventType = parser.getEventType();
   1186 
   1187             String projectName = null;
   1188             String themeId = null;
   1189             Uri exportedMovieUri = null;
   1190             long lastSaved = 0;
   1191             long playheadPosMs = 0;
   1192             long durationMs = 0;
   1193             int zoomLevel = DEFAULT_ZOOM_LEVEL;
   1194             while (eventType != XmlPullParser.END_DOCUMENT) {
   1195                 String name = null;
   1196                 switch (eventType) {
   1197                     case XmlPullParser.START_TAG: {
   1198                         name = parser.getName();
   1199                         if (name.equalsIgnoreCase(TAG_PROJECT)) {
   1200                             projectName = parser.getAttributeValue("", ATTR_NAME);
   1201                             themeId = parser.getAttributeValue("", ATTR_THEME);
   1202                             lastSaved = Long.parseLong(parser.getAttributeValue("", ATTR_SAVED));
   1203                             playheadPosMs = Long.parseLong(parser.getAttributeValue("",
   1204                                     ATTR_PLAYHEAD_POSITION));
   1205                             durationMs = Long.parseLong(parser.getAttributeValue("",
   1206                                     ATTR_DURATION));
   1207                             zoomLevel = Integer.parseInt(parser.getAttributeValue("",
   1208                                     ATTR_ZOOM_LEVEL));
   1209                         } else if (name.equalsIgnoreCase(TAG_MOVIE)) {
   1210                             exportedMovieUri = Uri.parse(parser.getAttributeValue("", ATTR_URI));
   1211                         } else if (name.equalsIgnoreCase(TAG_DOWNLOAD)) {
   1212                             downloads.add(new Download(parser.getAttributeValue("", ATTR_URI),
   1213                                     parser.getAttributeValue("", ATTR_MIME),
   1214                                     parser.getAttributeValue("", ATTR_FILENAME),
   1215                                     Long.parseLong(parser.getAttributeValue("", ATTR_TIME))));
   1216                         }
   1217 
   1218                         break;
   1219                     }
   1220 
   1221                     default: {
   1222                         break;
   1223                     }
   1224                 }
   1225                 eventType = parser.next();
   1226             }
   1227 
   1228             return new VideoEditorProject(videoEditor, projectPath, projectName, lastSaved,
   1229                     playheadPosMs, durationMs, zoomLevel, exportedMovieUri, themeId, downloads);
   1230         } finally {
   1231             if (fis != null) {
   1232                 fis.close();
   1233             }
   1234         }
   1235     }
   1236 
   1237     /**
   1238      * Save the content to XML
   1239      */
   1240     public void saveToXml() throws IOException {
   1241         // Save the project metadata
   1242         final XmlSerializer serializer = Xml.newSerializer();
   1243         final StringWriter writer = new StringWriter();
   1244         serializer.setOutput(writer);
   1245         serializer.startDocument("UTF-8", true);
   1246         serializer.startTag("", TAG_PROJECT);
   1247         if (mProjectName != null) {
   1248             serializer.attribute("", ATTR_NAME, mProjectName);
   1249         }
   1250         if (mTheme != null) {
   1251             serializer.attribute("", ATTR_THEME, mTheme);
   1252         }
   1253 
   1254         serializer.attribute("", ATTR_PLAYHEAD_POSITION, Long.toString(mPlayheadPosMs));
   1255         serializer.attribute("", ATTR_DURATION, Long.toString(computeDuration()));
   1256         serializer.attribute("", ATTR_ZOOM_LEVEL, Integer.toString(mZoomLevel));
   1257 
   1258         mLastSaved = System.currentTimeMillis();
   1259         serializer.attribute("", ATTR_SAVED, Long.toString(mLastSaved));
   1260         if (mExportedMovieUri != null) {
   1261             serializer.startTag("", TAG_MOVIE);
   1262             serializer.attribute("", ATTR_URI, mExportedMovieUri.toString());
   1263             serializer.endTag("", TAG_MOVIE);
   1264         }
   1265 
   1266         for (Download download : mDownloads) {
   1267             serializer.startTag("", TAG_DOWNLOAD);
   1268             serializer.attribute("", ATTR_URI, download.getMediaUri());
   1269             serializer.attribute("", ATTR_MIME, download.getMimeType());
   1270             serializer.attribute("", ATTR_FILENAME, download.getFilename());
   1271             serializer.attribute("", ATTR_TIME, Long.toString(download.getTime()));
   1272             serializer.endTag("", TAG_DOWNLOAD);
   1273         }
   1274         serializer.endTag("", TAG_PROJECT);
   1275         serializer.endDocument();
   1276 
   1277         // Save the metadata XML file
   1278         final FileOutputStream out = new FileOutputStream(new File(mVideoEditor.getPath(),
   1279                 PROJECT_METADATA_FILENAME));
   1280         out.write(writer.toString().getBytes("UTF-8"));
   1281         out.flush();
   1282         out.close();
   1283     }
   1284 }
   1285