1 /* 2 * Copyright (C) 2012 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.gallery3d.app; 18 19 import android.app.ActionBar; 20 import android.app.Activity; 21 import android.app.ProgressDialog; 22 import android.content.ContentResolver; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.database.Cursor; 27 import android.media.MediaPlayer; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.Environment; 31 import android.os.Handler; 32 import android.provider.MediaStore; 33 import android.provider.MediaStore.Video; 34 import android.provider.MediaStore.Video.VideoColumns; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.Window; 38 import android.widget.TextView; 39 import android.widget.Toast; 40 import android.widget.VideoView; 41 42 import com.android.gallery3d.R; 43 import com.android.gallery3d.util.BucketNames; 44 45 import java.io.File; 46 import java.io.IOException; 47 import java.sql.Date; 48 import java.text.SimpleDateFormat; 49 50 public class TrimVideo extends Activity implements 51 MediaPlayer.OnErrorListener, 52 MediaPlayer.OnCompletionListener, 53 ControllerOverlay.Listener { 54 55 private VideoView mVideoView; 56 private TrimControllerOverlay mController; 57 private Context mContext; 58 private Uri mUri; 59 private final Handler mHandler = new Handler(); 60 public static final String TRIM_ACTION = "com.android.camera.action.TRIM"; 61 62 public ProgressDialog mProgress; 63 64 private int mTrimStartTime = 0; 65 private int mTrimEndTime = 0; 66 private int mVideoPosition = 0; 67 public static final String KEY_TRIM_START = "trim_start"; 68 public static final String KEY_TRIM_END = "trim_end"; 69 public static final String KEY_VIDEO_POSITION = "video_pos"; 70 private boolean mHasPaused = false; 71 72 private String mSrcVideoPath = null; 73 private String mSaveFileName = null; 74 private static final String TIME_STAMP_NAME = "'TRIM'_yyyyMMdd_HHmmss"; 75 private File mSrcFile = null; 76 private File mDstFile = null; 77 private File mSaveDirectory = null; 78 // For showing the result. 79 private String saveFolderName = null; 80 81 @Override 82 public void onCreate(Bundle savedInstanceState) { 83 mContext = getApplicationContext(); 84 super.onCreate(savedInstanceState); 85 86 requestWindowFeature(Window.FEATURE_ACTION_BAR); 87 requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); 88 89 ActionBar actionBar = getActionBar(); 90 int displayOptions = ActionBar.DISPLAY_SHOW_HOME; 91 actionBar.setDisplayOptions(0, displayOptions); 92 displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM; 93 actionBar.setDisplayOptions(displayOptions, displayOptions); 94 actionBar.setCustomView(R.layout.trim_menu); 95 96 TextView mSaveVideoTextView = (TextView) findViewById(R.id.start_trim); 97 mSaveVideoTextView.setOnClickListener(new View.OnClickListener() { 98 @Override 99 public void onClick(View arg0) { 100 trimVideo(); 101 } 102 }); 103 104 Intent intent = getIntent(); 105 mUri = intent.getData(); 106 mSrcVideoPath = intent.getStringExtra(PhotoPage.KEY_MEDIA_ITEM_PATH); 107 setContentView(R.layout.trim_view); 108 View rootView = findViewById(R.id.trim_view_root); 109 110 mVideoView = (VideoView) rootView.findViewById(R.id.surface_view); 111 112 mController = new TrimControllerOverlay(mContext); 113 ((ViewGroup) rootView).addView(mController.getView()); 114 mController.setListener(this); 115 mController.setCanReplay(true); 116 117 mVideoView.setOnErrorListener(this); 118 mVideoView.setOnCompletionListener(this); 119 mVideoView.setVideoURI(mUri); 120 121 playVideo(); 122 } 123 124 @Override 125 public void onResume() { 126 super.onResume(); 127 if (mHasPaused) { 128 mVideoView.seekTo(mVideoPosition); 129 mVideoView.resume(); 130 mHasPaused = false; 131 } 132 mHandler.post(mProgressChecker); 133 } 134 135 @Override 136 public void onPause() { 137 mHasPaused = true; 138 mHandler.removeCallbacksAndMessages(null); 139 mVideoPosition = mVideoView.getCurrentPosition(); 140 mVideoView.suspend(); 141 super.onPause(); 142 } 143 144 @Override 145 public void onStop() { 146 if (mProgress != null) { 147 mProgress.dismiss(); 148 mProgress = null; 149 } 150 super.onStop(); 151 } 152 153 @Override 154 public void onDestroy() { 155 mVideoView.stopPlayback(); 156 super.onDestroy(); 157 } 158 159 private final Runnable mProgressChecker = new Runnable() { 160 @Override 161 public void run() { 162 int pos = setProgress(); 163 mHandler.postDelayed(mProgressChecker, 200 - (pos % 200)); 164 } 165 }; 166 167 @Override 168 public void onSaveInstanceState(Bundle savedInstanceState) { 169 savedInstanceState.putInt(KEY_TRIM_START, mTrimStartTime); 170 savedInstanceState.putInt(KEY_TRIM_END, mTrimEndTime); 171 savedInstanceState.putInt(KEY_VIDEO_POSITION, mVideoPosition); 172 super.onSaveInstanceState(savedInstanceState); 173 } 174 175 @Override 176 public void onRestoreInstanceState(Bundle savedInstanceState) { 177 super.onRestoreInstanceState(savedInstanceState); 178 mTrimStartTime = savedInstanceState.getInt(KEY_TRIM_START, 0); 179 mTrimEndTime = savedInstanceState.getInt(KEY_TRIM_END, 0); 180 mVideoPosition = savedInstanceState.getInt(KEY_VIDEO_POSITION, 0); 181 } 182 183 // This updates the time bar display (if necessary). It is called by 184 // mProgressChecker and also from places where the time bar needs 185 // to be updated immediately. 186 private int setProgress() { 187 mVideoPosition = mVideoView.getCurrentPosition(); 188 // If the video position is smaller than the starting point of trimming, 189 // correct it. 190 if (mVideoPosition < mTrimStartTime) { 191 mVideoView.seekTo(mTrimStartTime); 192 mVideoPosition = mTrimStartTime; 193 } 194 // If the position is bigger than the end point of trimming, show the 195 // replay button and pause. 196 if (mVideoPosition >= mTrimEndTime && mTrimEndTime > 0) { 197 if (mVideoPosition > mTrimEndTime) { 198 mVideoView.seekTo(mTrimEndTime); 199 mVideoPosition = mTrimEndTime; 200 } 201 mController.showEnded(); 202 mVideoView.pause(); 203 } 204 205 int duration = mVideoView.getDuration(); 206 if (duration > 0 && mTrimEndTime == 0) { 207 mTrimEndTime = duration; 208 } 209 mController.setTimes(mVideoPosition, duration, mTrimStartTime, mTrimEndTime); 210 return mVideoPosition; 211 } 212 213 private void playVideo() { 214 mVideoView.start(); 215 mController.showPlaying(); 216 setProgress(); 217 } 218 219 private void pauseVideo() { 220 mVideoView.pause(); 221 mController.showPaused(); 222 } 223 224 // Copy from SaveCopyTask.java in terms of how to handle the destination 225 // path and filename : querySource() and getSaveDirectory(). 226 private interface ContentResolverQueryCallback { 227 void onCursorResult(Cursor cursor); 228 } 229 230 private void querySource(String[] projection, ContentResolverQueryCallback callback) { 231 ContentResolver contentResolver = getContentResolver(); 232 Cursor cursor = null; 233 try { 234 cursor = contentResolver.query(mUri, projection, null, null, null); 235 if ((cursor != null) && cursor.moveToNext()) { 236 callback.onCursorResult(cursor); 237 } 238 } catch (Exception e) { 239 // Ignore error for lacking the data column from the source. 240 } finally { 241 if (cursor != null) { 242 cursor.close(); 243 } 244 } 245 } 246 247 private File getSaveDirectory() { 248 final File[] dir = new File[1]; 249 querySource(new String[] { 250 VideoColumns.DATA }, new ContentResolverQueryCallback() { 251 252 @Override 253 public void onCursorResult(Cursor cursor) { 254 dir[0] = new File(cursor.getString(0)).getParentFile(); 255 } 256 }); 257 return dir[0]; 258 } 259 260 private void trimVideo() { 261 int delta = mTrimEndTime - mTrimStartTime; 262 // Considering that we only trim at sync frame, we don't want to trim 263 // when the time interval is too short or too close to the origin. 264 if (delta < 100 ) { 265 Toast.makeText(getApplicationContext(), 266 getString(R.string.trim_too_short), 267 Toast.LENGTH_SHORT).show(); 268 return; 269 } 270 if (Math.abs(mVideoView.getDuration() - delta) < 100) { 271 // If no change has been made, go back 272 onBackPressed(); 273 return; 274 } 275 // Use the default save directory if the source directory cannot be 276 // saved. 277 mSaveDirectory = getSaveDirectory(); 278 if ((mSaveDirectory == null) || !mSaveDirectory.canWrite()) { 279 mSaveDirectory = new File(Environment.getExternalStorageDirectory(), 280 BucketNames.DOWNLOAD); 281 saveFolderName = getString(R.string.folder_download); 282 } else { 283 saveFolderName = mSaveDirectory.getName(); 284 } 285 mSaveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format( 286 new Date(System.currentTimeMillis())); 287 288 mDstFile = new File(mSaveDirectory, mSaveFileName + ".mp4"); 289 mSrcFile = new File(mSrcVideoPath); 290 291 showProgressDialog(); 292 293 new Thread(new Runnable() { 294 @Override 295 public void run() { 296 try { 297 TrimVideoUtils.startTrim(mSrcFile, mDstFile, mTrimStartTime, mTrimEndTime); 298 // Update the database for adding a new video file. 299 insertContent(mDstFile); 300 } catch (IOException e) { 301 e.printStackTrace(); 302 } 303 // After trimming is done, trigger the UI changed. 304 mHandler.post(new Runnable() { 305 @Override 306 public void run() { 307 Toast.makeText(getApplicationContext(), 308 getString(R.string.save_into) + " " + saveFolderName, 309 Toast.LENGTH_SHORT) 310 .show(); 311 // TODO: change trimming into a service to avoid 312 // this progressDialog and add notification properly. 313 if (mProgress != null) { 314 mProgress.dismiss(); 315 mProgress = null; 316 // Show the result only when the activity not stopped. 317 Intent intent = new Intent(android.content.Intent.ACTION_VIEW); 318 intent.setDataAndTypeAndNormalize(Uri.fromFile(mDstFile), "video/*"); 319 intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false); 320 startActivity(intent); 321 finish(); 322 } 323 } 324 }); 325 } 326 }).start(); 327 } 328 329 private void showProgressDialog() { 330 // create a background thread to trim the video. 331 // and show the progress. 332 mProgress = new ProgressDialog(this); 333 mProgress.setTitle(getString(R.string.trimming)); 334 mProgress.setMessage(getString(R.string.please_wait)); 335 // TODO: make this cancelable. 336 mProgress.setCancelable(false); 337 mProgress.setCanceledOnTouchOutside(false); 338 mProgress.show(); 339 } 340 341 /** 342 * Insert the content (saved file) with proper video properties. 343 */ 344 private Uri insertContent(File file) { 345 long nowInMs = System.currentTimeMillis(); 346 long nowInSec = nowInMs / 1000; 347 final ContentValues values = new ContentValues(12); 348 values.put(Video.Media.TITLE, mSaveFileName); 349 values.put(Video.Media.DISPLAY_NAME, file.getName()); 350 values.put(Video.Media.MIME_TYPE, "video/mp4"); 351 values.put(Video.Media.DATE_TAKEN, nowInMs); 352 values.put(Video.Media.DATE_MODIFIED, nowInSec); 353 values.put(Video.Media.DATE_ADDED, nowInSec); 354 values.put(Video.Media.DATA, file.getAbsolutePath()); 355 values.put(Video.Media.SIZE, file.length()); 356 // Copy the data taken and location info from src. 357 String[] projection = new String[] { 358 VideoColumns.DATE_TAKEN, 359 VideoColumns.LATITUDE, 360 VideoColumns.LONGITUDE, 361 VideoColumns.RESOLUTION, 362 }; 363 364 // Copy some info from the source file. 365 querySource(projection, new ContentResolverQueryCallback() { 366 @Override 367 public void onCursorResult(Cursor cursor) { 368 long timeTaken = cursor.getLong(0); 369 if (timeTaken > 0) { 370 values.put(Video.Media.DATE_TAKEN, timeTaken); 371 } 372 double latitude = cursor.getDouble(1); 373 double longitude = cursor.getDouble(2); 374 // TODO: Change || to && after the default location issue is 375 // fixed. 376 if ((latitude != 0f) || (longitude != 0f)) { 377 values.put(Video.Media.LATITUDE, latitude); 378 values.put(Video.Media.LONGITUDE, longitude); 379 } 380 values.put(Video.Media.RESOLUTION, cursor.getString(3)); 381 382 } 383 }); 384 385 return getContentResolver().insert(Video.Media.EXTERNAL_CONTENT_URI, values); 386 } 387 388 @Override 389 public void onPlayPause() { 390 if (mVideoView.isPlaying()) { 391 pauseVideo(); 392 } else { 393 playVideo(); 394 } 395 } 396 397 @Override 398 public void onSeekStart() { 399 pauseVideo(); 400 } 401 402 @Override 403 public void onSeekMove(int time) { 404 mVideoView.seekTo(time); 405 } 406 407 @Override 408 public void onSeekEnd(int time, int start, int end) { 409 mVideoView.seekTo(time); 410 mTrimStartTime = start; 411 mTrimEndTime = end; 412 setProgress(); 413 } 414 415 @Override 416 public void onShown() { 417 } 418 419 @Override 420 public void onHidden() { 421 } 422 423 @Override 424 public void onReplay() { 425 mVideoView.seekTo(mTrimStartTime); 426 playVideo(); 427 } 428 429 @Override 430 public void onCompletion(MediaPlayer mp) { 431 mController.showEnded(); 432 } 433 434 @Override 435 public boolean onError(MediaPlayer mp, int what, int extra) { 436 return false; 437 } 438 } 439