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.widgets; 18 19 import java.util.List; 20 21 import android.app.Activity; 22 import android.app.Dialog; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.os.Bundle; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.view.ActionMode; 29 import android.view.Display; 30 import android.view.Menu; 31 import android.view.MenuItem; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.widget.LinearLayout; 35 import android.widget.RelativeLayout; 36 import android.widget.SeekBar; 37 import android.widget.TextView; 38 39 import com.android.videoeditor.AlertDialogs; 40 import com.android.videoeditor.VideoEditorActivity; 41 import com.android.videoeditor.service.ApiService; 42 import com.android.videoeditor.service.MovieAudioTrack; 43 import com.android.videoeditor.service.VideoEditorProject; 44 import com.android.videoeditor.util.FileUtils; 45 import com.android.videoeditor.R; 46 47 /** 48 * The LinearLayout which displays audio tracks 49 */ 50 public class AudioTrackLinearLayout extends LinearLayout { 51 // Logging 52 private static final String TAG = "AudioTrackLinearLayout"; 53 54 // Dialog parameter ids 55 private static final String PARAM_DIALOG_AUDIO_TRACK_ID = "audio_track_id"; 56 57 // Instance variables 58 private final ItemSimpleGestureListener mAudioTrackGestureListener; 59 private final int mAudioTrackHeight; 60 private final int mHalfParentWidth; 61 private final View mAddAudioTrackButtonView; 62 private final int mAddAudioTrackButtonWidth; 63 private AudioTracksLayoutListener mListener; 64 private ActionMode mAudioTrackActionMode; 65 private VideoEditorProject mProject; 66 private boolean mPlaybackInProgress; 67 private long mTimelineDurationMs; 68 69 /** 70 * Activity listener 71 */ 72 public interface AudioTracksLayoutListener { 73 /** 74 * Add an audio track 75 */ 76 public void onAddAudioTrack(); 77 } 78 79 /** 80 * The audio track action mode handler 81 */ 82 private class AudioTrackActionModeCallback implements ActionMode.Callback, 83 SeekBar.OnSeekBarChangeListener { 84 // Instance variables 85 private final MovieAudioTrack mAudioTrack; 86 87 /** 88 * Constructor 89 * 90 * @param audioTrack The audio track 91 */ 92 public AudioTrackActionModeCallback(MovieAudioTrack audioTrack) { 93 mAudioTrack = audioTrack; 94 } 95 96 @Override 97 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 98 mAudioTrackActionMode = mode; 99 100 mode.getMenuInflater().inflate(R.menu.audio_mode_menu, menu); 101 102 final View titleBarView = inflate(getContext(), R.layout.audio_track_action_bar, null); 103 104 mode.setCustomView(titleBarView); 105 106 final TextView titleView = (TextView)titleBarView.findViewById(R.id.action_bar_title); 107 titleView.setText(FileUtils.getSimpleName(mAudioTrack.getFilename())); 108 109 final SeekBar seekBar = 110 ((SeekBar)titleBarView.findViewById(R.id.action_volume)); 111 seekBar.setOnSeekBarChangeListener(this); 112 seekBar.setProgress(mAudioTrack.getAppVolume()); 113 114 return true; 115 } 116 117 @Override 118 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 119 MenuItem duckItem = menu.findItem(R.id.action_duck); 120 duckItem.setChecked(mAudioTrack.isAppDuckingEnabled()); 121 return true; 122 } 123 124 @Override 125 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 126 switch (item.getItemId()) { 127 case R.id.action_duck: { 128 final boolean duck = !mAudioTrack.isAppDuckingEnabled(); 129 mAudioTrack.enableAppDucking(duck); 130 ApiService.setAudioTrackDuck(getContext(), mProject.getPath(), 131 mAudioTrack.getId(), duck); 132 item.setChecked(duck); 133 break; 134 } 135 136 case R.id.action_remove_audio_track: { 137 final Bundle bundle = new Bundle(); 138 bundle.putString(PARAM_DIALOG_AUDIO_TRACK_ID, mAudioTrack.getId()); 139 ((Activity)getContext()).showDialog( 140 VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID, bundle); 141 break; 142 } 143 } 144 return true; 145 } 146 147 @Override 148 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 149 if (fromUser) { 150 mAudioTrack.setAppVolume(progress); 151 ApiService.setAudioTrackVolume(getContext(), mProject.getPath(), 152 mAudioTrack.getId(), progress); 153 } 154 } 155 156 @Override 157 public void onStartTrackingTouch(SeekBar seekBar) { 158 } 159 160 @Override 161 public void onStopTrackingTouch(SeekBar seekBar) { 162 } 163 164 @Override 165 public void onDestroyActionMode(ActionMode mode) { 166 final View audioTrackView = getAudioTrackView(mAudioTrack.getId()); 167 if (audioTrackView != null) { 168 selectView(audioTrackView, false); 169 } 170 171 mAudioTrackActionMode = null; 172 } 173 } 174 175 public AudioTrackLinearLayout(Context context, AttributeSet attrs, int defStyle) { 176 super(context, attrs, defStyle); 177 178 mAudioTrackGestureListener = new ItemSimpleGestureListener() { 179 @Override 180 public boolean onSingleTapConfirmed(View view, int area, MotionEvent e) { 181 if (mPlaybackInProgress) { 182 return false; 183 } 184 185 if (!view.isSelected()) { 186 selectView(view, true); 187 } 188 189 return true; 190 } 191 192 @Override 193 public void onLongPress(View view, MotionEvent e) { 194 if (mPlaybackInProgress) { 195 return; 196 } 197 198 if (!view.isSelected()) { 199 selectView(view, true); 200 } 201 202 if (mAudioTrackActionMode == null) { 203 startActionMode(new AudioTrackActionModeCallback( 204 (MovieAudioTrack)view.getTag())); 205 } 206 } 207 }; 208 209 // Add the beginning timeline item 210 final View beginView = inflate(getContext(), R.layout.empty_timeline_item, null); 211 beginView.setOnClickListener(new View.OnClickListener() { 212 @Override 213 public void onClick(View view) { 214 unselectAllViews(); 215 } 216 }); 217 addView(beginView); 218 219 // Add the end timeline item 220 final View endView = inflate(context, R.layout.empty_timeline_item, null); 221 endView.setOnClickListener(new View.OnClickListener() { 222 @Override 223 public void onClick(View view) { 224 unselectAllViews(); 225 } 226 }); 227 addView(endView); 228 229 // Add the audio track button 230 mAddAudioTrackButtonView = inflate(getContext(), R.layout.add_audio_track_button, null); 231 addView(mAddAudioTrackButtonView, 1); 232 mAddAudioTrackButtonView.setOnClickListener(new View.OnClickListener() { 233 @Override 234 public void onClick(View view) { 235 if (mListener != null) { 236 mListener.onAddAudioTrack(); 237 } 238 } 239 }); 240 mAddAudioTrackButtonWidth = (int)context.getResources().getDimension( 241 R.dimen.add_audio_track_button_width); 242 243 // Compute half the width of the screen (and therefore the parent view) 244 final Display display = ((Activity)context).getWindowManager().getDefaultDisplay(); 245 mHalfParentWidth = display.getWidth() / 2; 246 247 // Get the layout height 248 mAudioTrackHeight = (int)context.getResources().getDimension(R.dimen.audio_layout_height); 249 250 setMotionEventSplittingEnabled(false); 251 } 252 253 public AudioTrackLinearLayout(Context context, AttributeSet attrs) { 254 this(context, attrs, 0); 255 } 256 257 public AudioTrackLinearLayout(Context context) { 258 this(context, null, 0); 259 } 260 261 /** 262 * The activity was resumed 263 */ 264 public void onResume() { 265 final int childrenCount = getChildCount(); 266 for (int i = 0; i < childrenCount; i++) { 267 final View childView = getChildAt(i); 268 final Object tag = childView.getTag(); 269 if (tag != null) { // This view represents an audio track 270 final AudioTrackView audioTrackView = (AudioTrackView)childView; 271 if (audioTrackView.getWaveformData() == null) { 272 final MovieAudioTrack audioTrack = (MovieAudioTrack)tag; 273 if (audioTrack.getWaveformData() != null) { 274 audioTrackView.setWaveformData(audioTrack.getWaveformData()); 275 audioTrackView.invalidate(); 276 } 277 } 278 } 279 } 280 } 281 282 /** 283 * @param listener The listener 284 */ 285 public void setListener(AudioTracksLayoutListener listener) { 286 mListener = listener; 287 } 288 289 /** 290 * @param project The project 291 */ 292 public void setProject(VideoEditorProject project) { 293 // Close the contextual action bar 294 if (mAudioTrackActionMode != null) { 295 mAudioTrackActionMode.finish(); 296 mAudioTrackActionMode = null; 297 } 298 299 mProject = project; 300 301 updateAddAudioTrackButton(); 302 303 removeAudioTrackViews(); 304 } 305 306 /** 307 * @param inProgress true if playback is in progress 308 */ 309 public void setPlaybackInProgress(boolean inProgress) { 310 mPlaybackInProgress = inProgress; 311 312 // Don't allow the user to interact with the audio tracks while playback 313 // is in progress 314 if (inProgress && mAudioTrackActionMode != null) { 315 mAudioTrackActionMode.finish(); 316 mAudioTrackActionMode = null; 317 } 318 } 319 320 /** 321 * Add the audio tracks 322 * 323 * @param audioTracks The audio tracks 324 */ 325 public void addAudioTracks(List<MovieAudioTrack> audioTracks) { 326 if (mAudioTrackActionMode != null) { 327 mAudioTrackActionMode.finish(); 328 mAudioTrackActionMode = null; 329 } 330 331 updateAddAudioTrackButton(); 332 333 removeAudioTrackViews(); 334 335 mTimelineDurationMs = mProject.computeDuration(); 336 337 for (MovieAudioTrack audioTrack : audioTracks) { 338 addAudioTrack(audioTrack); 339 } 340 } 341 342 /** 343 * Add a new audio track 344 * 345 * @param audioTrack The audio track 346 * 347 * @return The view that was added 348 */ 349 public View addAudioTrack(MovieAudioTrack audioTrack) { 350 updateAddAudioTrackButton(); 351 352 final AudioTrackView audioTrackView = (AudioTrackView)inflate(getContext(), 353 R.layout.audio_track_item, null); 354 355 audioTrackView.setTag(audioTrack); 356 357 audioTrackView.setGestureListener(mAudioTrackGestureListener); 358 359 audioTrackView.updateTimelineDuration(mTimelineDurationMs); 360 361 if (audioTrack.getWaveformData() != null) { 362 audioTrackView.setWaveformData(audioTrack.getWaveformData()); 363 } else { 364 ApiService.extractAudioTrackAudioWaveform(getContext(), mProject.getPath(), 365 audioTrack.getId()); 366 } 367 368 final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 369 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.FILL_PARENT); 370 addView(audioTrackView, getChildCount() - 1, lp); 371 372 if (mAudioTrackActionMode != null) { 373 mAudioTrackActionMode.invalidate(); 374 } 375 376 requestLayout(); 377 return audioTrackView; 378 } 379 380 /** 381 * Remove an audio track 382 * 383 * @param audioTrackId The audio track id 384 * @return The view which was removed 385 */ 386 public View removeAudioTrack(String audioTrackId) { 387 final int childrenCount = getChildCount(); 388 for (int i = 0; i < childrenCount; i++) { 389 final View childView = getChildAt(i); 390 final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag(); 391 if (audioTrack != null && audioTrack.getId().equals(audioTrackId)) { 392 removeViewAt(i); 393 394 updateAddAudioTrackButton(); 395 396 requestLayout(); 397 return childView; 398 } 399 } 400 401 return null; 402 } 403 404 /** 405 * Update the audio track item 406 * 407 * @param audioTrackId The audio track id 408 */ 409 public void updateAudioTrack(String audioTrackId) { 410 final AudioTrackView audioTrackView = (AudioTrackView)getAudioTrackView(audioTrackId); 411 if (audioTrackView == null) { 412 Log.e(TAG, "updateAudioTrack: audio track view not found: " + audioTrackId); 413 return; 414 } 415 416 if (mAudioTrackActionMode != null) { 417 mAudioTrackActionMode.invalidate(); 418 } 419 420 requestLayout(); 421 invalidate(); 422 } 423 424 /** 425 * An audio track is being decoded 426 * 427 * @param audioTrackId The audio track id 428 * @param action The action 429 * @param progress The progress 430 */ 431 public void onGeneratePreviewProgress(String audioTrackId, int action, int progress) { 432 final AudioTrackView audioTrackView = (AudioTrackView)getAudioTrackView(audioTrackId); 433 if (audioTrackView == null) { 434 Log.e(TAG, "onGeneratePreviewProgress: audio track view not found: " + audioTrackId); 435 return; 436 } 437 438 audioTrackView.setProgress(progress); 439 } 440 441 /** 442 * Set the waveform progress 443 * 444 * @param audioTrackId The audio track id 445 * @param progress The progress 446 */ 447 public void setWaveformExtractionProgress(String audioTrackId, int progress) { 448 final AudioTrackView audioTrackView = (AudioTrackView)getAudioTrackView(audioTrackId); 449 if (audioTrackView == null) { 450 Log.e(TAG, "setWaveformExtractionProgress: audio track view not found: " 451 + audioTrackId); 452 return; 453 } 454 455 audioTrackView.setProgress(progress); 456 } 457 458 /** 459 * The waveform extraction is complete 460 * 461 * @param audioTrackId The audio track id 462 */ 463 public void setWaveformExtractionComplete(String audioTrackId) { 464 final AudioTrackView audioTrackView = (AudioTrackView)getAudioTrackView(audioTrackId); 465 if (audioTrackView == null) { 466 Log.e(TAG, "setWaveformExtractionComplete: audio track view not found: " 467 + audioTrackId); 468 return; 469 } 470 471 audioTrackView.setProgress(-1); 472 473 final MovieAudioTrack audioTrack = (MovieAudioTrack)audioTrackView.getTag(); 474 if (audioTrack.getWaveformData() != null) { 475 audioTrackView.setWaveformData(audioTrack.getWaveformData()); 476 } 477 478 requestLayout(); 479 invalidate(); 480 } 481 482 /** 483 * The timeline duration has changed. Refresh the view. 484 */ 485 public void updateTimelineDuration() { 486 mTimelineDurationMs = mProject.computeDuration(); 487 488 // Media items may had been added or removed 489 updateAddAudioTrackButton(); 490 491 // Update the project duration for all views 492 final int childrenCount = getChildCount(); 493 for (int i = 0; i < childrenCount; i++) { 494 final View childView = getChildAt(i); 495 final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag(); 496 if (audioTrack != null) { 497 ((AudioTrackView)childView).updateTimelineDuration(mTimelineDurationMs); 498 } 499 } 500 501 requestLayout(); 502 invalidate(); 503 } 504 505 @Override 506 protected void onLayout(boolean changed, int l, int t, int r, int b) { 507 final int childrenCount = getChildCount(); 508 if (mTimelineDurationMs == 0) { 509 int left = 0; 510 for (int i = 0; i < childrenCount; i++) { 511 final View childView = getChildAt(i); 512 final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag(); 513 if (audioTrack != null) { 514 // Audio tracks are not visible 515 childView.layout(left, 0, left, mAudioTrackHeight); 516 } else { // Beginning and end views 517 childView.layout(left, 0, left + mHalfParentWidth, mAudioTrackHeight); 518 left += mHalfParentWidth; 519 } 520 } 521 } else { 522 final int viewWidth = getWidth() - (2 * mHalfParentWidth); 523 int left = 0; 524 525 final int leftViewWidth = (Integer)((View)getParent().getParent()).getTag( 526 R.id.left_view_width); 527 528 for (int i = 0; i < childrenCount; i++) { 529 final View childView = getChildAt(i); 530 final int id = childView.getId(); 531 final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag(); 532 if (audioTrack != null) { // Audio track views 533 final int width; 534 if (audioTrack.isAppLooping()) { 535 width = (int)((mTimelineDurationMs - 536 audioTrack.getAppStartTime()) * viewWidth / mTimelineDurationMs); 537 } else { 538 if (audioTrack.getAppStartTime() + audioTrack.getTimelineDuration() > 539 mTimelineDurationMs) { 540 width = (int)((mTimelineDurationMs - 541 audioTrack.getAppStartTime()) * viewWidth / mTimelineDurationMs); 542 } else { 543 width = (int)(audioTrack.getTimelineDuration() * viewWidth / 544 mTimelineDurationMs); 545 } 546 } 547 548 final int trackLeft = 549 (int)((audioTrack.getAppStartTime() * viewWidth) / mTimelineDurationMs) + 550 leftViewWidth; 551 childView.layout(trackLeft, 0, trackLeft + width, mAudioTrackHeight); 552 left = trackLeft + width; 553 } else if (id == R.id.add_audio_track_button) { 554 if (childView.getVisibility() == View.VISIBLE) { 555 childView.layout(left, 0, left + mAddAudioTrackButtonWidth, 556 mAudioTrackHeight); 557 left += mAddAudioTrackButtonWidth; 558 } 559 } else if (i == 0) { // Begin view 560 childView.layout(left, 0, left + leftViewWidth, mAudioTrackHeight); 561 left += leftViewWidth; 562 } else { // End view 563 childView.layout(left, 0, getWidth(), mAudioTrackHeight); 564 } 565 } 566 } 567 } 568 569 /** 570 * Create a new dialog 571 * 572 * @param id The dialog id 573 * @param bundle The dialog bundle 574 * 575 * @return The dialog 576 */ 577 public Dialog onCreateDialog(int id, final Bundle bundle) { 578 // If the project is not yet loaded do nothing. 579 if (mProject == null) { 580 return null; 581 } 582 583 switch (id) { 584 case VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID: { 585 final MovieAudioTrack audioTrack = mProject.getAudioTrack( 586 bundle.getString(PARAM_DIALOG_AUDIO_TRACK_ID)); 587 if (audioTrack == null) { 588 return null; 589 } 590 591 final Activity activity = (Activity)getContext(); 592 return AlertDialogs.createAlert(activity, 593 FileUtils.getSimpleName(audioTrack.getFilename()), 0, 594 activity.getString(R.string.editor_remove_audio_track_question), 595 activity.getString(R.string.yes), 596 new DialogInterface.OnClickListener() { 597 @Override 598 public void onClick(DialogInterface dialog, int which) { 599 if (mAudioTrackActionMode != null) { 600 mAudioTrackActionMode.finish(); 601 mAudioTrackActionMode = null; 602 } 603 activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID); 604 605 ApiService.removeAudioTrack(activity, mProject.getPath(), 606 audioTrack.getId()); 607 } 608 }, activity.getString(R.string.no), new DialogInterface.OnClickListener() { 609 @Override 610 public void onClick(DialogInterface dialog, int which) { 611 activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID); 612 } 613 }, new DialogInterface.OnCancelListener() { 614 @Override 615 public void onCancel(DialogInterface dialog) { 616 activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID); 617 } 618 }, true); 619 } 620 621 default: { 622 return null; 623 } 624 } 625 } 626 627 /** 628 * Find the audio track view with the specified id 629 * 630 * @param audioTrackId The audio track id 631 * @return The audio track view 632 */ 633 private View getAudioTrackView(String audioTrackId) { 634 final int childrenCount = getChildCount(); 635 for (int i = 0; i < childrenCount; i++) { 636 final View childView = getChildAt(i); 637 final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag(); 638 if (audioTrack != null && audioTrackId.equals(audioTrack.getId())) { 639 return childView; 640 } 641 } 642 643 return null; 644 } 645 646 /** 647 * Remove all audio track views (leave the beginning and end views) 648 */ 649 private void removeAudioTrackViews() { 650 int index = 0; 651 while (index < getChildCount()) { 652 final Object tag = getChildAt(index).getTag(); 653 if (tag != null) { 654 removeViewAt(index); 655 } else { 656 index++; 657 } 658 } 659 660 requestLayout(); 661 } 662 663 /** 664 * Set the background of the begin view 665 */ 666 private void updateAddAudioTrackButton() { 667 if (mProject == null) { // No project 668 mAddAudioTrackButtonView.setVisibility(View.GONE); 669 } else if (mProject.getMediaItemCount() > 0) { 670 if (mProject.getAudioTracks().size() > 0) { 671 mAddAudioTrackButtonView.setVisibility(View.GONE); 672 } else { 673 mAddAudioTrackButtonView.setVisibility(View.VISIBLE); 674 } 675 } else { // No media items 676 mAddAudioTrackButtonView.setVisibility(View.GONE); 677 } 678 } 679 680 @Override 681 public void setSelected(boolean selected) { 682 if (selected == false) { 683 // Close the contextual action bar 684 if (mAudioTrackActionMode != null) { 685 mAudioTrackActionMode.finish(); 686 mAudioTrackActionMode = null; 687 } 688 } 689 690 final int childrenCount = getChildCount(); 691 for (int i = 0; i < childrenCount; i++) { 692 final View childView = getChildAt(i); 693 childView.setSelected(false); 694 } 695 } 696 697 /** 698 * Select a view and unselect any view that is selected. 699 * 700 * @param selectedView The view to select 701 * @param selected true if selected 702 */ 703 private void selectView(View selectedView, boolean selected) { 704 // Check if the selection has changed 705 if (selectedView.isSelected() == selected) { 706 return; 707 } 708 709 if (selected) { 710 unselectAllViews(); 711 } 712 713 if (selected && mAudioTrackActionMode == null) { 714 startActionMode(new AudioTrackActionModeCallback( 715 (MovieAudioTrack)selectedView.getTag())); 716 } 717 718 // Select the new view 719 selectedView.setSelected(selected); 720 } 721 722 /** 723 * Unselect all views 724 */ 725 private void unselectAllViews() { 726 ((RelativeLayout)getParent()).setSelected(false); 727 } 728 } 729