/* * Copyright (C) 2008 Google Inc. * * 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.ringdroid; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.PrintWriter; import java.io.RandomAccessFile; import java.util.Random; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.database.Cursor; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.RingtoneManager; import android.media.MediaPlayer.OnCompletionListener; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.provider.MediaStore; import android.text.Editable; import android.text.TextWatcher; import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.AbsoluteLayout; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import com.ringdroid.soundfile.CheapSoundFile; //import entagged.audioformats.generic.Utils; /** * The activity for the Ringdroid main editor window. Keeps track of the * waveform display, current horizontal offset, marker handles, start / end text * boxes, and handles all of the buttons and controls. */ public class RingdroidEditActivity extends Activity implements MarkerView.MarkerListener, WaveformView.WaveformListener { private long mLoadingStartTime; private long mLoadingLastUpdateTime; private boolean mLoadingKeepGoing; private ProgressDialog mProgressDialog; private CheapSoundFile mSoundFile; private File mFile; private String mFilename; private String mDstFilename; private String mArtist; private String mAlbum; private String mGenre; private String mTitle; private int mYear; private String mExtension; private String mRecordingFilename; private int mNewFileKind; private Uri mRecordingUri; private boolean mWasGetContentIntent; private WaveformView mWaveformView; private MarkerView mStartMarker; private MarkerView mEndMarker; private TextView mStartText; private TextView mEndText; private TextView mInfo; private ImageButton mPlayButton; private ImageButton mRewindButton; private ImageButton mFfwdButton; private ImageButton mZoomInButton; private ImageButton mZoomOutButton; private ImageButton mSaveButton; private boolean mKeyDown; private String mCaption = ""; private int mWidth; private int mMaxPos; private int mStartPos; private int mEndPos; private int mLastDisplayedStartPos; private int mLastDisplayedEndPos; private int mOffset; private int mOffsetGoal; private int mPlayStartMsec; private int mPlayStartOffset; private int mPlayEndMsec; private Handler mHandler; private boolean mIsPlaying; private MediaPlayer mPlayer; private boolean mCanSeekAccurately; private boolean mTouchDragging; private float mTouchStart; private int mTouchInitialOffset; private int mTouchInitialStartPos; private int mTouchInitialEndPos; private long mWaveformTouchStartMsec; private float mDensity; private static final int kMarkerLeftInset = 46; private static final int kMarkerRightInset = 48; private static final int kMarkerTopOffset = 10; private static final int kMarkerBottomOffset = 10; // Menu commands private static final int CMD_SAVE = 1; private static final int CMD_RESET = 2; private static final int CMD_ABOUT = 3; private static final int CMD_USE_ALL = 4; // Dialog ID private static final int DIALOG_USE_ALL = 1; // Result codes private static final int REQUEST_CODE_RECORD = 1; private static final int REQUEST_CODE_CHOOSE_CONTACT = 2; /** * This is a special intent action that means "edit a sound file". */ public static final String EDIT = "com.ringdroid.action.EDIT"; /** * Preference names */ public static final String PREF_SUCCESS_COUNT = "success_count"; public static final String PREF_STATS_SERVER_CHECK = "stats_server_check"; public static final String PREF_STATS_SERVER_ALLOWED = "stats_server_allowed"; public static final String PREF_ERROR_COUNT = "error_count"; public static final String PREF_ERR_SERVER_CHECK = "err_server_check"; public static final String PREF_ERR_SERVER_ALLOWED = "err_server_allowed"; public static final String PREF_UNIQUE_ID = "unique_id"; /** * Possible codes for PREF_*_SERVER_ALLOWED */ public static final int SERVER_ALLOWED_UNKNOWN = 0; public static final int SERVER_ALLOWED_NO = 1; public static final int SERVER_ALLOWED_YES = 2; /** * Server url */ public static final String STATS_SERVER_URL = "http://ringdroid.appspot.com/add"; public static final String ERR_SERVER_URL = "http://ringdroid.appspot.com/err"; // // Public methods and protected overrides // /** Called with the activity is first created. */ @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); mRecordingFilename = null; mRecordingUri = null; mPlayer = null; mIsPlaying = false; Intent intent = getIntent(); mFilename = intent.getData().toString(); // If the Ringdroid media select activity was launched via a // GET_CONTENT intent, then we shouldn't display a "saved" // message when the user saves, we should just return whatever // they create. mWasGetContentIntent = intent.getBooleanExtra("was_get_content_intent", false); mSoundFile = null; mKeyDown = false; if (mFilename.equals("record")) { try { Intent recordIntent = new Intent( MediaStore.Audio.Media.RECORD_SOUND_ACTION); startActivityForResult(recordIntent, REQUEST_CODE_RECORD); } catch (Exception e) { showFinalAlert(e, R.string.record_error); } } loadGui(); mHandler = new Handler(); mHandler.postDelayed(mTimerRunnable, 100); if (!mFilename.equals("record")) { loadFromFile(); } } /** Called with the activity is finally destroyed. */ @Override protected void onDestroy() { Log.i("Ringdroid", "EditActivity OnDestroy"); if (mPlayer != null && mPlayer.isPlaying()) { mPlayer.stop(); } mPlayer = null; // if (mRecordingFilename != null) { // try { // if (!new File(mRecordingFilename).delete()) { // showFinalAlert(new Exception(), R.string.delete_tmp_error); // } // // getContentResolver().delete(mRecordingUri, null, null); // } catch (SecurityException e) { // showFinalAlert(e, R.string.delete_tmp_error); // } // } super.onDestroy(); } /** Called with an Activity we started with an Intent returns. */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent dataIntent) { if (requestCode == REQUEST_CODE_CHOOSE_CONTACT) { // The user finished saving their ringtone and they're // just applying it to a contact. When they return here, // they're done. // sendStatsToServerIfAllowedAndFinish(); return; } if (requestCode != REQUEST_CODE_RECORD) { return; } if (resultCode != RESULT_OK) { finish(); return; } if (dataIntent == null) { finish(); return; } // Get the recorded file and open it, but save the uri and // filename so that we can delete them when we exit; the // recorded file is only temporary and only the edited & saved // ringtone / other sound will stick around. mRecordingUri = dataIntent.getData(); mRecordingFilename = getFilenameFromUri(mRecordingUri); if (mRecordingFilename == null) return; mFilename = mRecordingFilename; loadFromFile(); } /** * Called when the orientation changes and/or the keyboard is shown or * hidden. We don't need to recreate the whole activity in this case, but we * do need to redo our layout somewhat. */ @Override public void onConfigurationChanged(Configuration newConfig) { final int saveZoomLevel = mWaveformView.getZoomLevel(); super.onConfigurationChanged(newConfig); loadGui(); enableZoomButtons(); new Handler().postDelayed(new Runnable() { public void run() { mStartMarker.requestFocus(); markerFocus(mStartMarker); mWaveformView.setZoomLevel(saveZoomLevel); mWaveformView.recomputeHeights(mDensity); updateDisplay(); } }, 500); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuItem item; item = menu.add(0, CMD_SAVE, 0, R.string.menu_save); item.setIcon(R.drawable.menu_save); item = menu.add(0, CMD_RESET, 0, R.string.menu_reset); item.setIcon(R.drawable.menu_reset); // item = menu.add(0, CMD_ABOUT, 0, R.string.menu_about); // item.setIcon(R.drawable.menu_about); item = menu.add(0, CMD_USE_ALL, 0, R.string.use_all); item.setIcon(R.drawable.ic_menu_chat_dashboard); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); menu.findItem(CMD_SAVE).setVisible(true); menu.findItem(CMD_RESET).setVisible(true); menu.findItem(CMD_USE_ALL).setVisible(true); // menu.findItem(CMD_ABOUT).setVisible(true); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case CMD_SAVE: if (mSoundFile == null) return false; if (mSoundFile.getAvgBitrateKbps() <= 32) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.rate_low).setCancelable(false) .setPositiveButton(R.string.use_all, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { showDialog(DIALOG_USE_ALL); } }).setNeutralButton(R.string.edit, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { onSave(); } }).setNegativeButton( R.string.alertdialog_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { } }); AlertDialog alert = builder.create(); alert.show(); } else { onSave(); } return true; case CMD_RESET: resetPositions(); mOffsetGoal = 0; updateDisplay(); return true; case CMD_ABOUT: onAbout(this); return true; case CMD_USE_ALL: onUseAll(); return true; default: return false; } } // // WaveformListener // /** * Every time we get a message that our waveform drew, see if we need to * animate and trigger another redraw. */ public void waveformDraw() { mWidth = mWaveformView.getMeasuredWidth(); if (mOffsetGoal != mOffset && !mKeyDown) updateDisplay(); else if (mIsPlaying) { updateDisplay(); } } public void waveformTouchStart(float x) { mTouchDragging = true; mTouchStart = x; mTouchInitialOffset = mOffset; mWaveformTouchStartMsec = System.currentTimeMillis(); } public void waveformTouchMove(float x) { mOffset = trap((int) (mTouchInitialOffset + (mTouchStart - x))); updateDisplay(); } public void waveformTouchEnd() { mTouchDragging = false; mOffsetGoal = mOffset; long elapsedMsec = System.currentTimeMillis() - mWaveformTouchStartMsec; if (elapsedMsec < 300) { if (mIsPlaying) { int seekMsec = mWaveformView .pixelsToMillisecs((int) (mTouchStart + mOffset)); if (seekMsec >= mPlayStartMsec && seekMsec < mPlayEndMsec) { mPlayer.seekTo(seekMsec - mPlayStartOffset); } else { handlePause(); } } else { onPlay((int) (mTouchStart + mOffset)); } } } // // MarkerListener // public void markerDraw() { } public void markerTouchStart(MarkerView marker, float x) { mTouchDragging = true; mTouchStart = x; mTouchInitialStartPos = mStartPos; mTouchInitialEndPos = mEndPos; } public void markerTouchMove(MarkerView marker, float x) { float delta = x - mTouchStart; if (marker == mStartMarker) { mStartPos = trap((int) (mTouchInitialStartPos + delta)); mEndPos = trap((int) (mTouchInitialEndPos + delta)); } else { mEndPos = trap((int) (mTouchInitialEndPos + delta)); if (mEndPos < mStartPos) mEndPos = mStartPos; } updateDisplay(); } public void markerTouchEnd(MarkerView marker) { mTouchDragging = false; if (marker == mStartMarker) { setOffsetGoalStart(); } else { setOffsetGoalEnd(); } } public void markerLeft(MarkerView marker, int velocity) { mKeyDown = true; if (marker == mStartMarker) { int saveStart = mStartPos; mStartPos = trap(mStartPos - velocity); mEndPos = trap(mEndPos - (saveStart - mStartPos)); setOffsetGoalStart(); } if (marker == mEndMarker) { if (mEndPos == mStartPos) { mStartPos = trap(mStartPos - velocity); mEndPos = mStartPos; } else { mEndPos = trap(mEndPos - velocity); } setOffsetGoalEnd(); } updateDisplay(); } public void markerRight(MarkerView marker, int velocity) { mKeyDown = true; if (marker == mStartMarker) { int saveStart = mStartPos; mStartPos += velocity; if (mStartPos > mMaxPos) mStartPos = mMaxPos; mEndPos += (mStartPos - saveStart); if (mEndPos > mMaxPos) mEndPos = mMaxPos; setOffsetGoalStart(); } if (marker == mEndMarker) { mEndPos += velocity; if (mEndPos > mMaxPos) mEndPos = mMaxPos; setOffsetGoalEnd(); } updateDisplay(); } public void markerEnter(MarkerView marker) { } public void markerKeyUp() { mKeyDown = false; updateDisplay(); } public void markerFocus(MarkerView marker) { mKeyDown = false; if (marker == mStartMarker) { setOffsetGoalStartNoUpdate(); } else { setOffsetGoalEndNoUpdate(); } // Delay updaing the display because if this focus was in // response to a touch event, we want to receive the touch // event too before updating the display. new Handler().postDelayed(new Runnable() { public void run() { updateDisplay(); } }, 100); } // // Static About dialog method, also called from RingdroidSelectActivity // public static void onAbout(final Activity activity) { new AlertDialog.Builder(activity).setTitle(R.string.about_title) .setMessage(R.string.about_text).setPositiveButton( R.string.alert_ok_button, null).setCancelable(false) .show(); } // // Internal methods // /** * Called from both onCreate and onConfigurationChanged (if the user * switched layouts) */ private void loadGui() { // Inflate our UI from its XML layout description. setContentView(R.layout.editor); DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); mDensity = metrics.density; mStartText = (TextView) findViewById(R.id.starttext); mStartText.addTextChangedListener(mTextWatcher); mEndText = (TextView) findViewById(R.id.endtext); mEndText.addTextChangedListener(mTextWatcher); mPlayButton = (ImageButton) findViewById(R.id.play); mPlayButton.setOnClickListener(mPlayListener); mRewindButton = (ImageButton) findViewById(R.id.rew); mRewindButton.setOnClickListener(mRewindListener); mFfwdButton = (ImageButton) findViewById(R.id.ffwd); mFfwdButton.setOnClickListener(mFfwdListener); mZoomInButton = (ImageButton) findViewById(R.id.zoom_in); mZoomInButton.setOnClickListener(mZoomInListener); mZoomOutButton = (ImageButton) findViewById(R.id.zoom_out); mZoomOutButton.setOnClickListener(mZoomOutListener); mSaveButton = (ImageButton) findViewById(R.id.save); mSaveButton.setOnClickListener(mSaveListener); TextView markStartButton = (TextView) findViewById(R.id.mark_start); markStartButton.setOnClickListener(mMarkStartListener); TextView markEndButton = (TextView) findViewById(R.id.mark_end); markEndButton.setOnClickListener(mMarkStartListener); enableDisableButtons(); mWaveformView = (WaveformView) findViewById(R.id.waveform); mWaveformView.setListener(this); mInfo = (TextView) findViewById(R.id.info); mInfo.setText(mCaption); mMaxPos = 0; mLastDisplayedStartPos = -1; mLastDisplayedEndPos = -1; if (mSoundFile != null) { mWaveformView.setSoundFile(mSoundFile); mWaveformView.recomputeHeights(mDensity); mMaxPos = mWaveformView.maxPos(); } mStartMarker = (MarkerView) findViewById(R.id.startmarker); mStartMarker.setListener(this); mStartMarker.setAlpha(255); mStartMarker.setFocusable(true); mStartMarker.setFocusableInTouchMode(true); mEndMarker = (MarkerView) findViewById(R.id.endmarker); mEndMarker.setListener(this); mEndMarker.setAlpha(255); mEndMarker.setFocusable(true); mEndMarker.setFocusableInTouchMode(true); updateDisplay(); } private void loadFromFile() { mFile = new File(mFilename); mExtension = getExtensionFromFilename(mFilename); SongMetadataReader metadataReader = new SongMetadataReader(this, mFilename); mTitle = metadataReader.mTitle; mArtist = metadataReader.mArtist; mAlbum = metadataReader.mAlbum; mYear = metadataReader.mYear; mGenre = metadataReader.mGenre; String titleLabel = mTitle; if (mArtist != null && mArtist.length() > 0) { titleLabel += " - " + mArtist; } setTitle(Utils.convertGBK(titleLabel)); mLoadingStartTime = System.currentTimeMillis(); mLoadingLastUpdateTime = System.currentTimeMillis(); mLoadingKeepGoing = true; mProgressDialog = new ProgressDialog(RingdroidEditActivity.this); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); mProgressDialog.setTitle(R.string.progress_dialog_loading); mProgressDialog.setCancelable(true); mProgressDialog .setOnCancelListener(new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { mLoadingKeepGoing = false; } }); mProgressDialog.show(); final CheapSoundFile.ProgressListener listener = new CheapSoundFile.ProgressListener() { public boolean reportProgress(double fractionComplete) { long now = System.currentTimeMillis(); if (now - mLoadingLastUpdateTime > 100) { mProgressDialog .setProgress((int) (mProgressDialog.getMax() * fractionComplete)); mLoadingLastUpdateTime = now; } return mLoadingKeepGoing; } }; // Create the MediaPlayer in a background thread mCanSeekAccurately = false; new Thread() { public void run() { mCanSeekAccurately = SeekTest .CanSeekAccurately(getPreferences(Context.MODE_PRIVATE)); System.out.println("Seek test done, creating media player."); try { MediaPlayer player = new MediaPlayer(); player.setDataSource(mFile.getAbsolutePath()); player.setAudioStreamType(AudioManager.STREAM_MUSIC); player.prepare(); mPlayer = player; } catch (final java.io.IOException e) { Runnable runnable = new Runnable() { public void run() { handleFatalError("ReadError", getResources() .getText(R.string.read_error), e); } }; mHandler.post(runnable); } ; } }.start(); // Load the sound file in a background thread new Thread() { public void run() { try { mSoundFile = CheapSoundFile.create(mFile.getAbsolutePath(), listener); if (mSoundFile == null) { String name = mFile.getName().toLowerCase(); String[] components = name.split("\\."); String err; if (components.length < 2) { err = getResources().getString( R.string.no_extension_error); } else { err = getResources().getString( R.string.bad_extension_error) + " " + components[components.length - 1]; } final String finalErr = err; Runnable runnable = new Runnable() { public void run() { if (!RingdroidEditActivity.this.isFinishing()) mProgressDialog.dismiss(); handleFatalError("UnsupportedExtension", finalErr, new Exception()); } }; mHandler.post(runnable); return; } } catch (final Exception e) { e.printStackTrace(); Runnable runnable = new Runnable() { public void run() { if (!RingdroidEditActivity.this.isFinishing()) mProgressDialog.dismiss(); mInfo.setText(e.toString()); handleFatalError("ReadError", getResources() .getText(R.string.read_error), e); } }; mHandler.post(runnable); return; } mHandler.post(new Runnable() { @Override public void run() { if (!RingdroidEditActivity.this.isFinishing()) mProgressDialog.dismiss(); } }); if (mLoadingKeepGoing) { Runnable runnable = new Runnable() { public void run() { finishOpeningSoundFile(); } }; mHandler.post(runnable); } else { RingdroidEditActivity.this.finish(); } } }.start(); } private void finishOpeningSoundFile() { mWaveformView.setSoundFile(mSoundFile); mWaveformView.recomputeHeights(mDensity); mMaxPos = mWaveformView.maxPos(); mLastDisplayedStartPos = -1; mLastDisplayedEndPos = -1; mTouchDragging = false; mOffset = 0; mOffsetGoal = 0; resetPositions(); if (mEndPos > mMaxPos) mEndPos = mMaxPos; mCaption = mSoundFile.getFiletype() + ", " + mSoundFile.getSampleRate() + " Hz, " + mSoundFile.getAvgBitrateKbps() + " kbps, " + formatTime(mMaxPos) + " " + getResources().getString(R.string.time_seconds); mInfo.setText(mCaption); updateDisplay(); } private synchronized void updateDisplay() { if (mIsPlaying && mPlayer != null) { int now = mPlayer.getCurrentPosition() + mPlayStartOffset; int frames = mWaveformView.millisecsToPixels(now); mWaveformView.setPlayback(frames); setOffsetGoalNoUpdate(frames - mWidth / 2); if (now >= mPlayEndMsec) { handlePause(); } } if (!mTouchDragging) { int offsetDelta = mOffsetGoal - mOffset; if (offsetDelta > 10) offsetDelta = offsetDelta / 10; else if (offsetDelta > 0) offsetDelta = 1; else if (offsetDelta < -10) offsetDelta = offsetDelta / 10; else if (offsetDelta < 0) offsetDelta = -1; else offsetDelta = 0; mOffset += offsetDelta; } mWaveformView.setParameters(mStartPos, mEndPos, mOffset); mWaveformView.invalidate(); int startX = mStartPos - mOffset - kMarkerLeftInset; if (startX + mStartMarker.getWidth() >= 0) { mStartMarker.setAlpha(255); } else { mStartMarker.setAlpha(0); startX = 0; } int endX = mEndPos - mOffset - mEndMarker.getWidth() + kMarkerRightInset; if (endX + mEndMarker.getWidth() >= 0) { mEndMarker.setAlpha(255); } else { mEndMarker.setAlpha(0); endX = 0; } mStartMarker.setLayoutParams(new AbsoluteLayout.LayoutParams( AbsoluteLayout.LayoutParams.WRAP_CONTENT, AbsoluteLayout.LayoutParams.WRAP_CONTENT, startX, kMarkerTopOffset)); mEndMarker.setLayoutParams(new AbsoluteLayout.LayoutParams( AbsoluteLayout.LayoutParams.WRAP_CONTENT, AbsoluteLayout.LayoutParams.WRAP_CONTENT, endX, mWaveformView .getMeasuredHeight() - mEndMarker.getHeight() - kMarkerBottomOffset)); } private Runnable mTimerRunnable = new Runnable() { public void run() { // Updating an EditText is slow on Android. Make sure // we only do the update if the text has actually changed. if (mStartPos != mLastDisplayedStartPos && !mStartText.hasFocus()) { mStartText.setText(formatTime(mStartPos)); mLastDisplayedStartPos = mStartPos; } if (mEndPos != mLastDisplayedEndPos && !mEndText.hasFocus()) { mEndText.setText(formatTime(mEndPos)); mLastDisplayedEndPos = mEndPos; } mHandler.postDelayed(mTimerRunnable, 100); } }; private void enableDisableButtons() { if (mIsPlaying) { mPlayButton.setImageResource(android.R.drawable.ic_media_pause); } else { mPlayButton.setImageResource(android.R.drawable.ic_media_play); } } private void resetPositions() { mStartPos = mWaveformView.secondsToPixels(0.0); mEndPos = mWaveformView.secondsToPixels(15.0); } private int trap(int pos) { if (pos < 0) return 0; if (pos > mMaxPos) return mMaxPos; return pos; } private void setOffsetGoalStart() { setOffsetGoal(mStartPos - mWidth / 2); } private void setOffsetGoalStartNoUpdate() { setOffsetGoalNoUpdate(mStartPos - mWidth / 2); } private void setOffsetGoalEnd() { setOffsetGoal(mEndPos - mWidth / 2); } private void setOffsetGoalEndNoUpdate() { setOffsetGoalNoUpdate(mEndPos - mWidth / 2); } private void setOffsetGoal(int offset) { setOffsetGoalNoUpdate(offset); updateDisplay(); } private void setOffsetGoalNoUpdate(int offset) { if (mTouchDragging) { return; } mOffsetGoal = offset; if (mOffsetGoal + mWidth / 2 > mMaxPos) mOffsetGoal = mMaxPos - mWidth / 2; if (mOffsetGoal < 0) mOffsetGoal = 0; } private String formatTime(int pixels) { if (mWaveformView != null && mWaveformView.isInitialized()) { return formatDecimal(mWaveformView.pixelsToSeconds(pixels)); } else { return ""; } } private String formatDecimal(double x) { int xWhole = (int) x; int xFrac = (int) (100 * (x - xWhole) + 0.5); if (xFrac >= 100) { xWhole++; // Round up xFrac -= 100; // Now we need the remainder after the round up if (xFrac < 10) { xFrac *= 10; // we need a fraction that is 2 digits long } } if (xFrac < 10) return xWhole + ".0" + xFrac; else return xWhole + "." + xFrac; } private synchronized void handlePause() { if (mPlayer != null && mPlayer.isPlaying()) { mPlayer.pause(); } mWaveformView.setPlayback(-1); mIsPlaying = false; enableDisableButtons(); } private synchronized void onPlay(int startPosition) { if (mIsPlaying) { handlePause(); return; } if (mPlayer == null) { // Not initialized yet return; } try { mPlayStartMsec = mWaveformView.pixelsToMillisecs(startPosition); if (startPosition < mStartPos) { mPlayEndMsec = mWaveformView.pixelsToMillisecs(mStartPos); } else if (startPosition > mEndPos) { mPlayEndMsec = mWaveformView.pixelsToMillisecs(mMaxPos); } else { mPlayEndMsec = mWaveformView.pixelsToMillisecs(mEndPos); } mPlayStartOffset = 0; int startFrame = mWaveformView .secondsToFrames(mPlayStartMsec * 0.001); int endFrame = mWaveformView.secondsToFrames(mPlayEndMsec * 0.001); int startByte = mSoundFile.getSeekableFrameOffset(startFrame); int endByte = mSoundFile.getSeekableFrameOffset(endFrame); if (mCanSeekAccurately && startByte >= 0 && endByte >= 0) { try { mPlayer.reset(); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); FileInputStream subsetInputStream = new FileInputStream( mFile.getAbsolutePath()); mPlayer.setDataSource(subsetInputStream.getFD(), startByte, endByte - startByte); mPlayer.prepare(); mPlayStartOffset = mPlayStartMsec; } catch (Exception e) { System.out.println("Exception trying to play file subset"); mPlayer.reset(); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mPlayer.setDataSource(mFile.getAbsolutePath()); mPlayer.prepare(); mPlayStartOffset = 0; } } mPlayer.setOnCompletionListener(new OnCompletionListener() { public synchronized void onCompletion(MediaPlayer arg0) { handlePause(); } }); mIsPlaying = true; if (mPlayStartOffset == 0) { mPlayer.seekTo(mPlayStartMsec); } mPlayer.start(); updateDisplay(); enableDisableButtons(); } catch (Exception e) { showFinalAlert(e, R.string.play_error); return; } } /** * Show a "final" alert dialog that will exit the activity after the user * clicks on the OK button. If an exception is passed, it's assumed to be an * error condition, and the dialog is presented as an error, and the stack * trace is logged. If there's no exception, it's a success message. */ private void showFinalAlert(Exception e, CharSequence message) { CharSequence title; if (e != null) { // Log.e("Ringdroid", "Error: " + message); // Log.e("Ringdroid", getStackTrace(e)); title = getResources().getText(R.string.alert_title_failure); setResult(RESULT_CANCELED, new Intent()); } else { Log.i("Ringdroid", "Success: " + message); title = getResources().getText(R.string.alert_title_success); } new AlertDialog.Builder(RingdroidEditActivity.this).setTitle(title) .setMessage(message).setPositiveButton( R.string.alert_ok_button, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { finish(); } }).setCancelable(false).show(); } private void showFinalAlert(Exception e, int messageResourceId) { showFinalAlert(e, getResources().getText(messageResourceId)); } private String makeRingtoneFilename(CharSequence title, String extension) { String parentdir; switch (mNewFileKind) { default: case FileSaveDialog.FILE_KIND_MUSIC: parentdir = "/sdcard/media/audio/music"; break; case FileSaveDialog.FILE_KIND_ALARM: parentdir = "/sdcard/media/audio/alarms"; break; case FileSaveDialog.FILE_KIND_NOTIFICATION: parentdir = "/sdcard/media/audio/notifications"; break; case FileSaveDialog.FILE_KIND_RINGTONE: parentdir = "/sdcard/media/audio/ringtones"; break; } // Create the parent directory File parentDirFile = new File(parentdir); parentDirFile.mkdirs(); // If we can't write to that special path, try just writing // directly to the sdcard if (!parentDirFile.isDirectory()) { parentdir = "/sdcard"; } // Turn the title into a filename String filename = ""; for (int i = 0; i < title.length(); i++) { if (Character.isLetterOrDigit(title.charAt(i))) { filename += title.charAt(i); } } // Try to make the filename unique String path = null; for (int i = 0; i < 100; i++) { String testPath; if (i > 0) testPath = parentdir + "/" + filename + i; else testPath = parentdir + "/" + filename; if (extension != null) { testPath += extension; } try { RandomAccessFile f = new RandomAccessFile(new File(testPath), "r"); } catch (Exception e) { // Good, the file didn't exist path = testPath; break; } } return path; } private void saveRingtone(final CharSequence title) { final String outPath = makeRingtoneFilename(title, mExtension); if (outPath == null) { showFinalAlert(new Exception(), R.string.no_unique_filename); return; } mDstFilename = outPath; double startTime = mWaveformView.pixelsToSeconds(mStartPos); double endTime = mWaveformView.pixelsToSeconds(mEndPos); final int startFrame = mWaveformView.secondsToFrames(startTime); final int endFrame = mWaveformView.secondsToFrames(endTime); final int duration = (int) (endTime - startTime + 0.5); // Create an indeterminate progress dialog mProgressDialog = new ProgressDialog(this); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); mProgressDialog.setTitle(R.string.progress_dialog_saving); mProgressDialog.setIndeterminate(true); mProgressDialog.setCancelable(false); mProgressDialog.show(); // Save the sound file in a background thread new Thread() { public void run() { final File outFile = new File(outPath); try { // Write the new file mSoundFile.WriteFile(outFile, startFrame, endFrame - startFrame); // Try to load the new file to make sure it worked final CheapSoundFile.ProgressListener listener = new CheapSoundFile.ProgressListener() { public boolean reportProgress(double frac) { // Do nothing - we're not going to try to // estimate when reloading a saved sound // since it's usually fast, but hard to // estimate anyway. return true; // Keep going } }; CheapSoundFile.create(outPath, listener); } catch (Exception e) { if (!RingdroidEditActivity.this.isFinishing()) mProgressDialog.dismiss(); CharSequence errorMessage; if (e.getMessage()!=null && e.getMessage().equals("No space left on device")) { errorMessage = getResources().getText( R.string.no_space_error); e = null; } else { errorMessage = getResources().getText( R.string.write_error); } final CharSequence finalErrorMessage = errorMessage; final Exception finalException = e; Runnable runnable = new Runnable() { public void run() { handleFatalError("WriteError", finalErrorMessage, finalException); } }; mHandler.post(runnable); return; } if (!RingdroidEditActivity.this.isFinishing()) mProgressDialog.dismiss(); Runnable runnable = new Runnable() { public void run() { afterSavingRingtone(title, outPath, outFile, duration); } }; mHandler.post(runnable); } }.start(); } private void afterSavingRingtone(CharSequence title, String outPath, File outFile, int duration) { long length = outFile.length(); if (length <= 512) { outFile.delete(); new AlertDialog.Builder(this) .setTitle(R.string.alert_title_failure).setMessage( R.string.too_small_error).setPositiveButton( R.string.alert_ok_button, null) .setCancelable(false).show(); return; } // Create the database record, pointing to the existing file path long fileSize = outFile.length(); String mimeType = "audio/mpeg"; String artist = "" + getResources().getText(R.string.artist_name); ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DATA, outPath); values.put(MediaStore.MediaColumns.TITLE, title.toString()); values.put(MediaStore.MediaColumns.SIZE, fileSize); values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); values.put(MediaStore.Audio.Media.ARTIST, artist); values.put(MediaStore.Audio.Media.DURATION, duration); values.put(MediaStore.Audio.Media.IS_RINGTONE, mNewFileKind == FileSaveDialog.FILE_KIND_RINGTONE); values.put(MediaStore.Audio.Media.IS_NOTIFICATION, mNewFileKind == FileSaveDialog.FILE_KIND_NOTIFICATION); values.put(MediaStore.Audio.Media.IS_ALARM, mNewFileKind == FileSaveDialog.FILE_KIND_ALARM); values.put(MediaStore.Audio.Media.IS_MUSIC, mNewFileKind == FileSaveDialog.FILE_KIND_MUSIC); // Insert it into the database Uri uri = MediaStore.Audio.Media.getContentUriForPath(outPath); final Uri newUri = getContentResolver().insert(uri, values); setResult(RESULT_OK, new Intent().setData(newUri)); // Update a preference that counts how many times we've // successfully saved a ringtone or other audio SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); int successCount = prefs.getInt(PREF_SUCCESS_COUNT, 0); SharedPreferences.Editor prefsEditor = prefs.edit(); prefsEditor.putInt(PREF_SUCCESS_COUNT, successCount + 1); prefsEditor.commit(); // If Ringdroid was launched to get content, just return if (mWasGetContentIntent) { // sendStatsToServerIfAllowedAndFinish(); return; } // There's nothing more to do with music or an alarm. Show a // success message and then quit. if (mNewFileKind == FileSaveDialog.FILE_KIND_MUSIC || mNewFileKind == FileSaveDialog.FILE_KIND_ALARM) { Toast.makeText(this, R.string.save_success_message, Toast.LENGTH_SHORT).show(); // sendStatsToServerIfAllowedAndFinish(); return; } // If it's a notification, give the user the option of making // this their default notification. If they say no, we're finished. if (mNewFileKind == FileSaveDialog.FILE_KIND_NOTIFICATION) { new AlertDialog.Builder(RingdroidEditActivity.this).setTitle( R.string.alert_title_success).setMessage( R.string.set_default_notification).setPositiveButton( R.string.alert_yes_button, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { RingtoneManager.setActualDefaultRingtoneUri( RingdroidEditActivity.this, RingtoneManager.TYPE_NOTIFICATION, newUri); // sendStatsToServerIfAllowedAndFinish(); } }).setNegativeButton(R.string.alert_no_button, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // sendStatsToServerIfAllowedAndFinish(); } }).setCancelable(false).show(); return; } // If we get here, that means the type is a ringtone. There are // three choices: make this your default ringtone, assign it to a // contact, or do nothing. final Handler handler = new Handler() { public void handleMessage(Message response) { int actionId = response.arg1; switch (actionId) { case R.id.button_make_default: RingtoneManager.setActualDefaultRingtoneUri( RingdroidEditActivity.this, RingtoneManager.TYPE_RINGTONE, newUri); Toast.makeText(RingdroidEditActivity.this, R.string.default_ringtone_success_message, Toast.LENGTH_SHORT).show(); // sendStatsToServerIfAllowedAndFinish(); break; case R.id.button_choose_contact: chooseContactForRingtone(newUri); break; default: case R.id.button_do_nothing: // sendStatsToServerIfAllowedAndFinish(); break; } } }; Message message = Message.obtain(handler); AfterSaveActionDialog dlog = new AfterSaveActionDialog(this, message); dlog.show(); } private void chooseContactForRingtone(Uri uri) { try { Intent intent = new Intent(Intent.ACTION_EDIT, uri); intent.setClassName(this, "com.ringdroid.ChooseContactActivity"); startActivityForResult(intent, REQUEST_CODE_CHOOSE_CONTACT); } catch (Exception e) { // Log.e("Ringdroid", "Couldn't open Choose Contact window"); } } private void handleFatalError(final CharSequence errorInternalName, final CharSequence errorString, final Exception exception) { Log.i("Ringdroid", "handleFatalError"); SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); int failureCount = prefs.getInt(PREF_ERROR_COUNT, 0); final SharedPreferences.Editor prefsEditor = prefs.edit(); prefsEditor.putInt(PREF_ERROR_COUNT, failureCount + 1); prefsEditor.commit(); // Check if we already have a pref for whether or not we can // contact the server. int serverAllowed = prefs.getInt(PREF_ERR_SERVER_ALLOWED, SERVER_ALLOWED_UNKNOWN); if (serverAllowed == SERVER_ALLOWED_NO) { Log.i("Ringdroid", "ERR: SERVER_ALLOWED_NO"); // Just show a simple "write error" message showFinalAlert(exception, errorString); return; } if (serverAllowed == SERVER_ALLOWED_YES) { Log.i("Ringdroid", "SERVER_ALLOWED_YES"); new AlertDialog.Builder(RingdroidEditActivity.this).setTitle( R.string.alert_title_failure).setMessage(errorString) .setPositiveButton(R.string.alert_ok_button, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { return; } }).setCancelable(false).show(); return; } // The number of times the user must have had a failure before // we'll ask them. Defaults to 1, and each time they click "Later" // we double and add 1. final int allowServerCheckIndex = prefs .getInt(PREF_ERR_SERVER_CHECK, 1); if (failureCount < allowServerCheckIndex) { Log.i("Ringdroid", "failureCount " + failureCount + " is less than " + allowServerCheckIndex); // Just show a simple "write error" message showFinalAlert(exception, errorString); return; } try { new AlertDialog.Builder(this).setTitle(R.string.alert_title_failure) .setMessage( errorString + ". " + getResources().getText( R.string.error_server_prompt)) .setPositiveButton(R.string.server_yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { prefsEditor.putInt(PREF_ERR_SERVER_ALLOWED, SERVER_ALLOWED_YES); prefsEditor.commit(); } }).setNeutralButton(R.string.server_later, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { prefsEditor.putInt(PREF_ERR_SERVER_CHECK, 1 + allowServerCheckIndex * 2); Log.i("Ringdroid", "Won't check again until " + (1 + allowServerCheckIndex * 2) + " errors."); prefsEditor.commit(); finish(); } }).setNegativeButton(R.string.server_never, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { prefsEditor.putInt(PREF_ERR_SERVER_ALLOWED, SERVER_ALLOWED_NO); prefsEditor.commit(); finish(); } }).setCancelable(false).show(); } catch (Exception e) { // .show(); crashes sometime due to no window token, // I guess it is android bug } } private void onSave() { if (mIsPlaying) { handlePause(); } final Activity activity = this; final Handler finishHandler = new Handler() { public void handleMessage(Message response) { activity.finish(); } }; final Handler handler = new Handler() { public void handleMessage(Message response) { CharSequence newTitle = (CharSequence) response.obj; mNewFileKind = response.arg1; saveRingtone(newTitle); } }; Message message = Message.obtain(handler); FileSaveDialog dlog = new FileSaveDialog(this, getResources(), Utils.convertGBK(mTitle), message); dlog.show(); } private void onUseAll() { if (mIsPlaying) { handlePause(); } showDialog(DIALOG_USE_ALL); } private int ring_button_type; @Override protected Dialog onCreateDialog(int id) { if (id == DIALOG_USE_ALL) { return new AlertDialog.Builder(this).setIcon( android.R.drawable.ic_dialog_alert).setTitle( R.string.ring_picker_title).setSingleChoiceItems( R.array.set_ring_option, 0, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { ring_button_type = whichButton; } }).setPositiveButton(R.string.alert_ok_button, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { int ring_type = 0; if (ring_button_type == 3) { Intent intent = new Intent(); Uri currentFileUri = Uri.parse(mFilename); intent.setData(currentFileUri); intent .setClass( RingdroidEditActivity.this, com.ringdroid.ChooseContactActivity.class); RingdroidEditActivity.this .startActivity(intent); return; } else if (ring_button_type == 0) { ring_type = RingtoneManager.TYPE_RINGTONE; } else if (ring_button_type == 1) { ring_type = RingtoneManager.TYPE_NOTIFICATION; } else if (ring_button_type == 2) { ring_type = RingtoneManager.TYPE_ALARM; } // u = Const.mp3dir + ring.getString(Const.mp3); Uri currentFileUri = null; if (!mFilename.startsWith("content:")) { String selection; selection = MediaStore.Audio.Media.DATA + "=?";//+"='"+DatabaseUtils.sqlEscapeString(mFilename)+"'"; String[] selectionArgs = new String[1]; selectionArgs[0] = mFilename; Uri head = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Cursor c = getExternalAudioCursor(selection, selectionArgs); if (c == null || c.getCount() == 0) { head = MediaStore.Audio.Media.INTERNAL_CONTENT_URI; c = getInternalAudioCursor(selection, selectionArgs); } startManagingCursor(c); if (c == null || c.getCount() == 0) return; c.moveToFirst(); String itemUri = head.toString() + "/" + c .getString(c .getColumnIndexOrThrow(MediaStore.Audio.Media._ID)); currentFileUri = Uri.parse(itemUri); } else { currentFileUri = Uri.parse(mFilename); } RingtoneManager.setActualDefaultRingtoneUri( RingdroidEditActivity.this, ring_type, currentFileUri); Toast.makeText(RingdroidEditActivity.this, "" + getResources().getStringArray(R.array.set_ring_option)[ring_button_type], Toast.LENGTH_SHORT).show(); } }).setNegativeButton(R.string.alertdialog_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }).create(); } return super.onCreateDialog(id); } private void enableZoomButtons() { mZoomInButton.setEnabled(mWaveformView.canZoomIn()); mZoomOutButton.setEnabled(mWaveformView.canZoomOut()); } private OnClickListener mSaveListener = new OnClickListener() { public void onClick(View sender) { if (mSoundFile == null) return; if (mSoundFile.getAvgBitrateKbps() <= 32) { AlertDialog.Builder builder = new AlertDialog.Builder( RingdroidEditActivity.this); builder.setMessage(R.string.rate_low).setCancelable(false) .setPositiveButton(R.string.use_all, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { showDialog(DIALOG_USE_ALL); } }).setNeutralButton(R.string.edit, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { onSave(); } }).setNegativeButton( R.string.alertdialog_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { } }); AlertDialog alert = builder.create(); alert.show(); } else { onSave(); } } }; private OnClickListener mPlayListener = new OnClickListener() { public void onClick(View sender) { onPlay(mStartPos); } }; private OnClickListener mZoomInListener = new OnClickListener() { public void onClick(View sender) { mWaveformView.zoomIn(); mStartPos = mWaveformView.getStart(); mEndPos = mWaveformView.getEnd(); mMaxPos = mWaveformView.maxPos(); mOffset = mWaveformView.getOffset(); mOffsetGoal = mOffset; enableZoomButtons(); updateDisplay(); } }; private OnClickListener mZoomOutListener = new OnClickListener() { public void onClick(View sender) { mWaveformView.zoomOut(); mStartPos = mWaveformView.getStart(); mEndPos = mWaveformView.getEnd(); mMaxPos = mWaveformView.maxPos(); mOffset = mWaveformView.getOffset(); mOffsetGoal = mOffset; enableZoomButtons(); updateDisplay(); } }; private OnClickListener mRewindListener = new OnClickListener() { public void onClick(View sender) { if (mIsPlaying) { int newPos = mPlayer.getCurrentPosition() - 5000; if (newPos < mPlayStartMsec) newPos = mPlayStartMsec; mPlayer.seekTo(newPos); } else { mStartMarker.requestFocus(); markerFocus(mStartMarker); } } }; private OnClickListener mFfwdListener = new OnClickListener() { public void onClick(View sender) { if (mIsPlaying) { int newPos = 5000 + mPlayer.getCurrentPosition(); if (newPos > mPlayEndMsec) newPos = mPlayEndMsec; mPlayer.seekTo(newPos); } else { mEndMarker.requestFocus(); markerFocus(mEndMarker); } } }; private OnClickListener mMarkStartListener = new OnClickListener() { public void onClick(View sender) { if (mIsPlaying) { mStartPos = mWaveformView.millisecsToPixels(mPlayer .getCurrentPosition() + mPlayStartOffset); updateDisplay(); } } }; private OnClickListener mMarkEndListener = new OnClickListener() { public void onClick(View sender) { if (mIsPlaying) { mEndPos = mWaveformView.millisecsToPixels(mPlayer .getCurrentPosition() + mPlayStartOffset); updateDisplay(); handlePause(); } } }; private TextWatcher mTextWatcher = new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { } public void afterTextChanged(Editable s) { if (mStartText.hasFocus()) { try { mStartPos = mWaveformView.secondsToPixels(Double .parseDouble(mStartText.getText().toString())); updateDisplay(); } catch (NumberFormatException e) { } } if (mEndText.hasFocus()) { try { mEndPos = mWaveformView.secondsToPixels(Double .parseDouble(mEndText.getText().toString())); updateDisplay(); } catch (NumberFormatException e) { } } } }; private String getStackTrace(Exception e) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); PrintWriter writer = new PrintWriter(stream, true); e.printStackTrace(writer); return stream.toString(); } /** * Return extension including dot, like ".mp3" */ private String getExtensionFromFilename(String filename) { int start = filename.lastIndexOf('.'); if (start > 0) return filename.substring(start, filename.length()); else return null; } private String getFilenameFromUri(Uri uri) { Cursor c = managedQuery(uri, null, "", null, null); if (c.getCount() == 0) { return null; } c.moveToFirst(); int dataIndex = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA); return c.getString(dataIndex); } /** * Nothing nefarious about this; the purpose is just to uniquely identify * each user so we don't double-count the same ringtone - without actually * identifying the actual user. */ long getUniqueId() { SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); long uniqueId = prefs.getLong(PREF_UNIQUE_ID, 0); if (uniqueId == 0) { uniqueId = new Random().nextLong(); SharedPreferences.Editor prefsEditor = prefs.edit(); prefsEditor.putLong(PREF_UNIQUE_ID, uniqueId); prefsEditor.commit(); } return uniqueId; } private Cursor getInternalAudioCursor(String selection, String[] selectionArgs) { return managedQuery(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS, selection, selectionArgs, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); } private Cursor getExternalAudioCursor(String selection, String[] selectionArgs) { return managedQuery(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, EXTERNAL_COLUMNS, selection, selectionArgs, MediaStore.Audio.Media.DATE_ADDED + " DESC"); } private static final String[] INTERNAL_COLUMNS = new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.IS_RINGTONE, MediaStore.Audio.Media.IS_ALARM, MediaStore.Audio.Media.IS_NOTIFICATION, MediaStore.Audio.Media.IS_MUSIC }; private static final String[] EXTERNAL_COLUMNS = new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.IS_RINGTONE, MediaStore.Audio.Media.IS_ALARM, MediaStore.Audio.Media.IS_NOTIFICATION, MediaStore.Audio.Media.IS_MUSIC}; }