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