/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.videoeditor; import java.util.List; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.media.AudioManager; import roughcut.media.videoeditor.AudioTrack; import roughcut.media.videoeditor.MediaItem; import roughcut.media.videoeditor.MediaVideoItem; import roughcut.media.videoeditor.Transition; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.widget.Toast; import com.example.android.videoeditor.service.ApiService; import com.example.android.videoeditor.service.ApiServiceListener; import com.example.android.videoeditor.service.MovieAudioTrack; import com.example.android.videoeditor.service.MovieEffect; import com.example.android.videoeditor.service.MovieMediaItem; import com.example.android.videoeditor.service.MovieOverlay; import com.example.android.videoeditor.service.MovieTransition; import com.example.android.videoeditor.service.VideoEditorProject; import com.example.android.videoeditor.widgets.AudioTrackLinearLayout; import com.example.android.videoeditor.widgets.MediaLinearLayout; import com.example.android.videoeditor.widgets.OverlayLinearLayout; /** * This activity handles callbacks from the service and manages the project */ public abstract class VideoEditorBaseActivity extends Activity { // Logging private static final String TAG = "VideoEditorBase"; protected static final int DIALOG_DELETE_BAD_PROJECT_ID = 100; // State keys private static final String STATE_PROJECT_PATH = "path"; private static final String STATE_EXPORT_FILENAME = "export_filename"; // Dialog parameters protected static final String PARAM_PROJECT_PATH = "path"; // Instance variables private final ServiceListener mServiceListener = new ServiceListener(); protected String mProjectPath; protected VideoEditorProject mProject; protected String mPendingExportFilename; private boolean mProjectEditState; /** * The service listener */ private class ServiceListener extends ApiServiceListener { @Override public void onProjectEditState(String projectPath, boolean projectEdited) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProjectEditState != projectEdited) { mProjectEditState = projectEdited; onProjectEditStateChange(projectEdited); } } @Override public void onVideoEditorCreated(String projectPath, VideoEditorProject project, List<MediaItem> mediaItems, List<AudioTrack> audioTracks, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } // Check if an error occurred if (exception != null) { // Invalidate the project path mProjectPath = null; enterDisabledState(R.string.editor_no_project); // Display an error Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_create_error, Toast.LENGTH_LONG).show(); } else { enterReadyState(); mProject = project; initializeFromProject(true); } } @Override public void onVideoEditorLoaded(String projectPath, VideoEditorProject project, List<MediaItem> mediaItems, List<AudioTrack> audioTracks, Exception exception) { if (!projectPath.equals(mProjectPath)) { return; } // Check if an error occurred if (exception != null || project == null) { mProjectPath = null; enterDisabledState(R.string.editor_no_project); final Bundle bundle = new Bundle(); bundle.putString(VideoEditorActivity.PARAM_PROJECT_PATH, projectPath); showDialog(DIALOG_DELETE_BAD_PROJECT_ID, bundle); } else { // The project may be loaded already. This can happen when we // create a new project if (mProject == null) { mProject = project; initializeFromProject(true); } } } @Override public void onVideoEditorAspectRatioSet(String projectPath, int aspectRatio, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_aspect_ratio_error, Toast.LENGTH_LONG).show(); } else { // The aspect ratio has changed setAspectRatio(aspectRatio); } } @Override public void onVideoEditorThemeApplied(String projectPath, String theme, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_apply_theme_error, Toast.LENGTH_LONG).show(); } else { getMediaLayout().addMediaItems(mProject.getMediaItems()); getOverlayLayout().addMediaItems(mProject.getMediaItems()); getAudioTrackLayout().addAudioTracks(mProject.getAudioTracks()); updateTimelineDuration(); } } @Override public void onVideoEditorGeneratePreviewProgress(String projectPath, String className, String itemId, int action, int progress) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "onVideoEditorGeneratePreviewProgress: " + className + " " + progress); } if (className == null) { // Last callback (after all items are generated) if (action == ApiService.ACTION_UPDATE_FRAME) { showPreviewFrame(); } } else if (MediaItem.class.getCanonicalName().equals(className)) { getMediaLayout().onGeneratePreviewMediaItemProgress(itemId, action, progress); } else if (Transition.class.getCanonicalName().equals(className)) { getMediaLayout().onGeneratePreviewTransitionProgress(itemId, action, progress); } else if (AudioTrack.class.getCanonicalName().equals(className)) { getAudioTrackLayout().onGeneratePreviewProgress(itemId, action, progress); } else { Log.w(TAG, "Unsupported storyboard item type: " + className); } } @Override public void onVideoEditorExportProgress(String projectPath, String filename, int progress) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (!filename.equals(mPendingExportFilename)) { return; } // Update the export progress onExportProgress(progress); } @Override public void onVideoEditorExportComplete(String projectPath, String filename, Exception exception, boolean cancelled) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (!filename.equals(mPendingExportFilename)) { return; } onExportComplete(); mPendingExportFilename = null; if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_export_error, Toast.LENGTH_LONG).show(); } } @Override public void onVideoEditorSaved(String projectPath, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_saved_error, Toast.LENGTH_LONG).show(); } } @Override public void onVideoEditorReleased(String projectPath, Exception exception) { if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_release_error, Toast.LENGTH_LONG).show(); } } @Override public void onVideoEditorDeleted(String projectPath, Exception exception) { if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_delete_error, Toast.LENGTH_LONG).show(); } } @Override public void onMediaItemAdded(String projectPath, String mediaItemId, MovieMediaItem mediaItem, String afterMediaItemId, Class<?> mediaItemClass, Integer newAspectRatio, Exception exception) { // Check if the VideoEditor is the one we are expecting. if (!projectPath.equals(mProjectPath) || mProject == null) { return; } if (exception != null) { if (mediaItemClass.getCanonicalName().equals( MediaVideoItem.class.getCanonicalName())) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_add_video_clip_error, Toast.LENGTH_LONG).show(); } else { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_add_image_error, Toast.LENGTH_LONG).show(); } } else { getMediaLayout().insertMediaItem(mediaItem, afterMediaItemId); getOverlayLayout().insertMediaItem(mediaItem, afterMediaItemId); if (newAspectRatio != null) { // The aspect ratio has changed setAspectRatio(newAspectRatio); } updateTimelineDuration(); } } @Override public void onMediaLoaded(String projectPath, Uri mediaIUri, String mimeType, String filename, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_media_load_error, Toast.LENGTH_LONG).show(); } else { // Update the status of the downloaded item in the user interface } } @Override public void onMediaItemMoved(String projectPath, String mediaItemId, String afterMediaItemId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_move_media_item_error, Toast.LENGTH_LONG).show(); } else { // Update the entire timeline getMediaLayout().addMediaItems(mProject.getMediaItems()); getOverlayLayout().addMediaItems(mProject.getMediaItems()); updateTimelineDuration(); } } @Override public void onMediaItemRemoved(String projectPath, String mediaItemId, MovieTransition transition, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_remove_media_item_error, Toast.LENGTH_LONG).show(); } else { // Remove the media item and bounding transitions getMediaLayout().removeMediaItem(mediaItemId, transition); getOverlayLayout().removeMediaItem(mediaItemId); updateTimelineDuration(); } } @Override public void onMediaItemRenderingModeSet(String projectPath, String mediaItemId, int renderingMode, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_set_rendering_mode_error, Toast.LENGTH_LONG).show(); } } @Override public void onMediaItemDurationSet(String projectPath, String mediaItemId, long durationMs, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_set_media_item_duration_error, Toast.LENGTH_LONG).show(); } else { final MovieMediaItem mediaItem = mProject.getMediaItem(mediaItemId); // Update the media item getMediaLayout().updateMediaItem(mediaItem); getOverlayLayout().updateMediaItem(mediaItem); updateTimelineDuration(); } } @Override public void onMediaItemBoundariesSet(String projectPath, String mediaItemId, long beginBoundaryMs, long endBoundaryMs, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_set_media_item_boundaries_error, Toast.LENGTH_LONG).show(); } else { final MovieMediaItem mediaItem = mProject.getMediaItem(mediaItemId); getMediaLayout().updateMediaItem(mediaItem); getOverlayLayout().updateMediaItem(mediaItem); // Place the cursor at the beginning of the trimmed media item //movePlayhead(mProject.getMediaItemBeginTime(mediaItemId)); updateTimelineDuration(); } } @Override public boolean onMediaItemThumbnail(String projectPath, String mediaItemId, Bitmap thumbnail, int index, int token, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return false; } if (mProject == null) { return false; } if (exception != null) { return false; } else { return getMediaLayout().setMediaItemThumbnail( mediaItemId, thumbnail, index, token); } } @Override public void onTransitionInserted(String projectPath, MovieTransition transition, String afterMediaId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_add_transition_error, Toast.LENGTH_LONG).show(); } else { getMediaLayout().addTransition(transition, afterMediaId); updateTimelineDuration(); } } @Override public void onTransitionRemoved(String projectPath, String transitionId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_remove_transition_error, Toast.LENGTH_LONG).show(); } else { getMediaLayout().removeTransition(transitionId); updateTimelineDuration(); } } @Override public void onTransitionDurationSet(String projectPath, String transitionId, long durationMs, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_set_transition_duration_error, Toast.LENGTH_LONG).show(); } else { getMediaLayout().updateTransition(transitionId); getOverlayLayout().refresh(); updateTimelineDuration(); } } @Override public boolean onTransitionThumbnails(String projectPath, String transitionId, Bitmap[] thumbnails, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return false; } if (mProject == null) { return false; } if (exception != null) { return false; } else { return getMediaLayout().setTransitionThumbnails(transitionId, thumbnails); } } @Override public void onOverlayAdded(String projectPath, MovieOverlay overlay, String mediaItemId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_add_overlay_error, Toast.LENGTH_LONG).show(); } else { getMediaLayout().invalidateActionBar(); getOverlayLayout().addOverlay(mediaItemId, overlay); } } @Override public void onOverlayRemoved(String projectPath, String overlayId, String mediaItemId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_remove_overlay_error, Toast.LENGTH_LONG).show(); } else { getOverlayLayout().removeOverlay(mediaItemId, overlayId); } } @Override public void onOverlayStartTimeSet(String projectPath, String overlayId, String mediaItemId, long startTimeMs, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_set_start_time_overlay_error, Toast.LENGTH_LONG).show(); } } @Override public void onOverlayDurationSet(String projectPath, String overlayId, String mediaItemId, long durationMs, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_set_duration_overlay_error, Toast.LENGTH_LONG).show(); } } @Override public void onOverlayUserAttributesSet(String projectPath, String overlayId, String mediaItemId, Bundle userAttributes, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_set_user_attributes_overlay_error, Toast.LENGTH_LONG).show(); } else { getOverlayLayout().updateOverlayAttributes(mediaItemId, overlayId, userAttributes); } } @Override public void onEffectAdded(String projectPath, MovieEffect effect, String mediaItemId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_add_effect_error, Toast.LENGTH_LONG).show(); } else { getMediaLayout().updateMediaItem(mProject.getMediaItem(mediaItemId)); } } @Override public void onEffectRemoved(String projectPath, String effectId, String mediaItemId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_remove_effect_error, Toast.LENGTH_LONG).show(); } else { // Remove the effect getMediaLayout().updateMediaItem(mProject.getMediaItem(mediaItemId)); } } @Override public void onAudioTrackAdded(String projectPath, MovieAudioTrack audioTrack, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_add_audio_track_error, Toast.LENGTH_LONG).show(); } else { getAudioTrackLayout().addAudioTrack(audioTrack); } } @Override public void onAudioTrackRemoved(String projectPath, String audioTrackId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_remove_audio_track_error, Toast.LENGTH_LONG).show(); } else { getAudioTrackLayout().removeAudioTrack(audioTrackId); } } @Override public void onAudioTrackBoundariesSet(String projectPath, String audioTrackId, long beginBoundaryMs, long endBoundaryMs, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception != null) { Toast.makeText(VideoEditorBaseActivity.this, R.string.editor_set_audio_track_boundaries_error, Toast.LENGTH_LONG).show(); } else { getAudioTrackLayout().updateAudioTrack(audioTrackId); } } @Override public void onAudioTrackExtractAudioWaveformProgress(String projectPath, String audioTrackId, int progress) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } getAudioTrackLayout().setWaveformExtractionProgress(audioTrackId, progress); } @Override public void onAudioTrackExtractAudioWaveformComplete(String projectPath, String audioTrackId, Exception exception) { // Check if the VideoEditor is the one we are expecting if (!projectPath.equals(mProjectPath)) { return; } if (mProject == null) { return; } if (exception == null) { getAudioTrackLayout().setWaveformExtractionComplete(audioTrackId); } } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.video_editor); if (savedInstanceState != null) { mProjectPath = savedInstanceState.getString(STATE_PROJECT_PATH); mPendingExportFilename = savedInstanceState.getString(STATE_EXPORT_FILENAME); } else { final Intent intent = getIntent(); mProjectPath = intent.getStringExtra(ProjectsActivity.PARAM_OPEN_PROJECT_PATH); if (Intent.ACTION_INSERT.equals(intent.getAction())) { ApiService.createVideoEditor(this, mProjectPath, intent.getStringExtra(ProjectsActivity.PARAM_CREATE_PROJECT_NAME), new String[0], new String[0], null); } } } @Override public void onResume() { super.onResume(); mProjectEditState = ApiService.isProjectBeingEdited(mProjectPath); onProjectEditStateChange(mProjectEditState); ApiService.registerListener(mServiceListener); // Check if we need to load the project if (mProjectPath != null) { if (mProject == null) { // We need to load the project ApiService.loadVideoEditor(this, mProjectPath); enterTransitionalState(R.string.editor_loading_project); } else { // The project is already loaded initializeFromProject(false); } } else { enterDisabledState(R.string.editor_no_project); } // Request the audio focus so that other apps can pause playback. final AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE); audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); } @Override public void onPause() { super.onPause(); ApiService.unregisterListener(mServiceListener); if (mProject != null) { // Mark the project as clean. If/when we resume the activity // we can check this flag in order to refresh the UI for changes // which may had occurred when the activity was paused. mProject.setClean(true); // Save the contents of the current project ApiService.saveVideoEditor(this, mProjectPath); } // Release the audio focus final AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE); audioManager.abandonAudioFocus(null); } @Override public void onDestroy() { super.onDestroy(); // If we have an active project release the VideoEditor if (mProjectPath != null) { if (!isChangingConfigurations()) { ApiService.releaseVideoEditor(this, mProjectPath); } mProjectPath = null; } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(STATE_PROJECT_PATH, mProjectPath); outState.putString(STATE_EXPORT_FILENAME, mPendingExportFilename); } /** * @return true if the project is edited */ protected boolean isProjectEdited() { return mProjectEditState; } /** * Enter the disabled state * * @param statusStringId The status string id */ protected abstract void enterDisabledState(int statusStringId); /** * Enter the transitional state * * @param statusStringId The status string id */ protected abstract void enterTransitionalState(int statusStringId); /** * Enter the ready state */ protected abstract void enterReadyState(); /** * Show the preview frame at the current playhead position * * @return true if the surface is created, false otherwise. */ protected abstract boolean showPreviewFrame(); /** * The duration of the timeline has changed */ protected abstract void updateTimelineDuration(); /** * Move the playhead to the specified position * * @param timeMs The time position */ protected abstract void movePlayhead(long timeMs); /** * Change the aspect ratio * * @param aspectRatio The new aspect ratio */ protected abstract void setAspectRatio(int aspectRatio); /** * Initialize the project when restored from storage. * * @param updateUI true to update the UI * * Note that this method may be called also after the project was loaded */ protected abstract void initializeFromProject(boolean updateUI); /** * @return The media layout */ protected abstract MediaLinearLayout getMediaLayout(); /** * @return The overlay layout */ protected abstract OverlayLinearLayout getOverlayLayout(); /** * @return The audio layout */ protected abstract AudioTrackLinearLayout getAudioTrackLayout(); /** * The export is progressing */ protected abstract void onExportProgress(int progress); /** * The export has completed */ protected abstract void onExportComplete(); /** * @param projectEdited true if the project is edited */ protected abstract void onProjectEditStateChange(boolean projectEdited); }