Merge "Music app: Handle audio VIEW and PICK in TrackBrowserActivity"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b8e337b..6adb4b3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -60,6 +60,63 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="AudioPreview" android:theme="@android:style/Theme.Dialog"
+                  android:taskAffinity=""
+                  android:excludeFromRecents="true" android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="file"/>
+                <data android:mimeType="audio/*"/>
+                <data android:mimeType="application/ogg"/>
+                <data android:mimeType="application/x-ogg"/>
+                <data android:mimeType="application/itunes"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:mimeType="audio/*"/>
+                <data android:mimeType="application/ogg"/>
+                <data android:mimeType="application/x-ogg"/>
+                <data android:mimeType="application/itunes"/>
+            </intent-filter>
+            <intent-filter
+                    android:priority="-1">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="content" />
+                <data android:mimeType="audio/*"/>
+                <data android:mimeType="application/ogg"/>
+                <data android:mimeType="application/x-ogg"/>
+                <data android:mimeType="application/itunes"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name="com.android.music.MusicPicker"
+                  android:label="@string/music_picker_title" android:exported="true" >
+            <!-- First way to invoke us: someone asks to get content of
+                 any of the audio types we support. -->
+            <intent-filter>
+                <action android:name="android.intent.action.GET_CONTENT" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.OPENABLE" />
+                <data android:mimeType="audio/*"/>
+                <data android:mimeType="application/ogg"/>
+                <data android:mimeType="application/x-ogg"/>
+            </intent-filter>
+            <!-- Second way to invoke us: someone asks to pick an item from
+                 some media Uri. -->
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.OPENABLE" />
+                <data android:mimeType="vnd.android.cursor.dir/audio"/>
+            </intent-filter>
+        </activity>
+
         <!--
             This is the "current music playing" panel, which has special
             launch behavior.  We clear its task affinity, so it will not
diff --git a/src/com/android/music/AudioPreview.java b/src/com/android/music/AudioPreview.java
new file mode 100644
index 0000000..c05884b
--- /dev/null
+++ b/src/com/android/music/AudioPreview.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.music;
+
+import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ImageButton;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.Toast;
+
+import java.io.IOException;
+
+/**
+ * Dialog that comes up in response to various music-related VIEW intents.
+ */
+public class AudioPreview
+        extends Activity implements OnPreparedListener, OnErrorListener, OnCompletionListener {
+    private final static String TAG = "AudioPreview";
+    private PreviewPlayer mPlayer;
+    private TextView mTextLine1;
+    private TextView mTextLine2;
+    private TextView mLoadingText;
+    private SeekBar mSeekBar;
+    private Handler mProgressRefresher;
+    private boolean mSeeking = false;
+    private boolean mUiPaused = true;
+    private int mDuration;
+    private Uri mUri;
+    private long mMediaId = -1;
+    private static final int OPEN_IN_MUSIC = 1;
+    private AudioManager mAudioManager;
+    private boolean mPausedByTransientLossOfFocus;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        Intent intent = getIntent();
+        if (intent == null) {
+            finish();
+            return;
+        }
+        mUri = intent.getData();
+        if (mUri == null) {
+            finish();
+            return;
+        }
+        String scheme = mUri.getScheme();
+
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.audiopreview);
+
+        mTextLine1 = (TextView) findViewById(R.id.line1);
+        mTextLine2 = (TextView) findViewById(R.id.line2);
+        mLoadingText = (TextView) findViewById(R.id.loading);
+        if (scheme.equals("http")) {
+            String msg = getString(R.string.streamloadingtext, mUri.getHost());
+            mLoadingText.setText(msg);
+        } else {
+            mLoadingText.setVisibility(View.GONE);
+        }
+        mSeekBar = (SeekBar) findViewById(R.id.progress);
+        mProgressRefresher = new Handler();
+        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+
+        PreviewPlayer player = (PreviewPlayer) getLastNonConfigurationInstance();
+        if (player == null) {
+            mPlayer = new PreviewPlayer();
+            mPlayer.setActivity(this);
+            try {
+                mPlayer.setDataSourceAndPrepare(mUri);
+            } catch (Exception ex) {
+                // catch generic Exception, since we may be called with a media
+                // content URI, another content provider's URI, a file URI,
+                // an http URI, and there are different exceptions associated
+                // with failure to open each of those.
+                Log.d(TAG, "Failed to open file: " + ex);
+                Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
+                finish();
+                return;
+            }
+        } else {
+            mPlayer = player;
+            mPlayer.setActivity(this);
+            // onResume will update the UI
+        }
+
+        AsyncQueryHandler mAsyncQueryHandler = new AsyncQueryHandler(getContentResolver()) {
+            @Override
+            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+                if (cursor != null && cursor.moveToFirst()) {
+                    int titleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
+                    int artistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
+                    int idIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
+                    int displaynameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+
+                    if (idIdx >= 0) {
+                        mMediaId = cursor.getLong(idIdx);
+                    }
+
+                    if (titleIdx >= 0) {
+                        String title = cursor.getString(titleIdx);
+                        mTextLine1.setText(title);
+                        if (artistIdx >= 0) {
+                            String artist = cursor.getString(artistIdx);
+                            mTextLine2.setText(artist);
+                        }
+                    } else if (displaynameIdx >= 0) {
+                        String name = cursor.getString(displaynameIdx);
+                        mTextLine1.setText(name);
+                    } else {
+                        // Couldn't find anything to display, what to do now?
+                        Log.w(TAG, "Cursor had no names for us");
+                    }
+                } else {
+                    Log.w(TAG, "empty cursor");
+                }
+
+                if (cursor != null) {
+                    cursor.close();
+                }
+                setNames();
+            }
+        };
+
+        if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
+            if (mUri.getAuthority() == MediaStore.AUTHORITY) {
+                // try to get title and artist from the media content provider
+                mAsyncQueryHandler.startQuery(0, null, mUri,
+                        new String[] {MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST},
+                        null, null, null);
+            } else {
+                // Try to get the display name from another content provider.
+                // Don't specifically ask for the display name though, since the
+                // provider might not actually support that column.
+                mAsyncQueryHandler.startQuery(0, null, mUri, null, null, null, null);
+            }
+        } else if (scheme.equals("file")) {
+            // check if this file is in the media database (clicking on a download
+            // in the download manager might follow this path
+            String path = mUri.getPath();
+            mAsyncQueryHandler.startQuery(0, null, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                    new String[] {MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE,
+                            MediaStore.Audio.Media.ARTIST},
+                    MediaStore.Audio.Media.DATA + "=?", new String[] {path}, null);
+        } else {
+            // We can't get metadata from the file/stream itself yet, because
+            // that API is hidden, so instead we display the URI being played
+            if (mPlayer.isPrepared()) {
+                setNames();
+            }
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mUiPaused = true;
+        if (mProgressRefresher != null) {
+            mProgressRefresher.removeCallbacksAndMessages(null);
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mUiPaused = false;
+        if (mPlayer.isPrepared()) {
+            showPostPrepareUI();
+        }
+    }
+
+    @Override
+    public Object onRetainNonConfigurationInstance() {
+        PreviewPlayer player = mPlayer;
+        mPlayer = null;
+        return player;
+    }
+
+    @Override
+    public void onDestroy() {
+        stopPlayback();
+        super.onDestroy();
+    }
+
+    private void stopPlayback() {
+        if (mProgressRefresher != null) {
+            mProgressRefresher.removeCallbacksAndMessages(null);
+        }
+        if (mPlayer != null) {
+            mPlayer.release();
+            mPlayer = null;
+            mAudioManager.abandonAudioFocus(mAudioFocusListener);
+        }
+    }
+
+    @Override
+    public void onUserLeaveHint() {
+        stopPlayback();
+        finish();
+        super.onUserLeaveHint();
+    }
+
+    public void onPrepared(MediaPlayer mp) {
+        if (isFinishing()) return;
+        mPlayer = (PreviewPlayer) mp;
+        setNames();
+        mPlayer.start();
+        showPostPrepareUI();
+    }
+
+    private void showPostPrepareUI() {
+        ProgressBar pb = (ProgressBar) findViewById(R.id.spinner);
+        pb.setVisibility(View.GONE);
+        mDuration = mPlayer.getDuration();
+        if (mDuration != 0) {
+            mSeekBar.setMax(mDuration);
+            mSeekBar.setVisibility(View.VISIBLE);
+            if (!mSeeking) {
+                mSeekBar.setProgress(mPlayer.getCurrentPosition());
+            }
+        }
+        mSeekBar.setOnSeekBarChangeListener(mSeekListener);
+        mLoadingText.setVisibility(View.GONE);
+        View v = findViewById(R.id.titleandbuttons);
+        v.setVisibility(View.VISIBLE);
+        mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
+                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        if (mProgressRefresher != null) {
+            mProgressRefresher.removeCallbacksAndMessages(null);
+            mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
+        }
+        updatePlayPause();
+    }
+
+    private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
+        public void onAudioFocusChange(int focusChange) {
+            if (mPlayer == null) {
+                // this activity has handed its MediaPlayer off to the next activity
+                // (e.g. portrait/landscape switch) and should abandon its focus
+                mAudioManager.abandonAudioFocus(this);
+                return;
+            }
+            switch (focusChange) {
+                case AudioManager.AUDIOFOCUS_LOSS:
+                    mPausedByTransientLossOfFocus = false;
+                    mPlayer.pause();
+                    break;
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                    if (mPlayer.isPlaying()) {
+                        mPausedByTransientLossOfFocus = true;
+                        mPlayer.pause();
+                    }
+                    break;
+                case AudioManager.AUDIOFOCUS_GAIN:
+                    if (mPausedByTransientLossOfFocus) {
+                        mPausedByTransientLossOfFocus = false;
+                        start();
+                    }
+                    break;
+            }
+            updatePlayPause();
+        }
+    };
+
+    private void start() {
+        mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
+                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        mPlayer.start();
+        mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
+    }
+
+    public void setNames() {
+        if (TextUtils.isEmpty(mTextLine1.getText())) {
+            mTextLine1.setText(mUri.getLastPathSegment());
+        }
+        if (TextUtils.isEmpty(mTextLine2.getText())) {
+            mTextLine2.setVisibility(View.GONE);
+        } else {
+            mTextLine2.setVisibility(View.VISIBLE);
+        }
+    }
+
+    class ProgressRefresher implements Runnable {
+        @Override
+        public void run() {
+            if (mPlayer != null && !mSeeking && mDuration != 0) {
+                mSeekBar.setProgress(mPlayer.getCurrentPosition());
+            }
+            mProgressRefresher.removeCallbacksAndMessages(null);
+            if (!mUiPaused) {
+                mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
+            }
+        }
+    }
+
+    private void updatePlayPause() {
+        ImageButton b = (ImageButton) findViewById(R.id.playpause);
+        if (b != null && mPlayer != null) {
+            if (mPlayer.isPlaying()) {
+                b.setImageResource(R.drawable.btn_playback_ic_pause_small);
+            } else {
+                b.setImageResource(R.drawable.btn_playback_ic_play_small);
+                mProgressRefresher.removeCallbacksAndMessages(null);
+            }
+        }
+    }
+
+    private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+        public void onStartTrackingTouch(SeekBar bar) {
+            mSeeking = true;
+        }
+        public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
+            if (!fromuser) {
+                return;
+            }
+            // Protection for case of simultaneously tapping on seek bar and exit
+            if (mPlayer == null) {
+                return;
+            }
+            mPlayer.seekTo(progress);
+        }
+        public void onStopTrackingTouch(SeekBar bar) {
+            mSeeking = false;
+        }
+    };
+
+    public boolean onError(MediaPlayer mp, int what, int extra) {
+        Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
+        finish();
+        return true;
+    }
+
+    public void onCompletion(MediaPlayer mp) {
+        mSeekBar.setProgress(mDuration);
+        updatePlayPause();
+    }
+
+    public void playPauseClicked(View v) {
+        // Protection for case of simultaneously tapping on play/pause and exit
+        if (mPlayer == null) {
+            return;
+        }
+        if (mPlayer.isPlaying()) {
+            mPlayer.pause();
+        } else {
+            start();
+        }
+        updatePlayPause();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        // TODO: if mMediaId != -1, then the playing file has an entry in the media
+        // database, and we could open it in the full music app instead.
+        // Ideally, we would hand off the currently running mediaplayer
+        // to the music UI, which can probably be done via a public static
+        menu.add(0, OPEN_IN_MUSIC, 0, "open in music");
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        MenuItem item = menu.findItem(OPEN_IN_MUSIC);
+        if (mMediaId >= 0) {
+            item.setVisible(true);
+            return true;
+        }
+        item.setVisible(false);
+        return false;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_HEADSETHOOK:
+            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+                if (mPlayer.isPlaying()) {
+                    mPlayer.pause();
+                } else {
+                    start();
+                }
+                updatePlayPause();
+                return true;
+            case KeyEvent.KEYCODE_MEDIA_PLAY:
+                start();
+                updatePlayPause();
+                return true;
+            case KeyEvent.KEYCODE_MEDIA_PAUSE:
+                if (mPlayer.isPlaying()) {
+                    mPlayer.pause();
+                }
+                updatePlayPause();
+                return true;
+            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+            case KeyEvent.KEYCODE_MEDIA_NEXT:
+            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+            case KeyEvent.KEYCODE_MEDIA_REWIND:
+                return true;
+            case KeyEvent.KEYCODE_MEDIA_STOP:
+            case KeyEvent.KEYCODE_BACK:
+                stopPlayback();
+                finish();
+                return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    /*
+     * Wrapper class to help with handing off the MediaPlayer to the next instance
+     * of the activity in case of orientation change, without losing any state.
+     */
+    private static class PreviewPlayer extends MediaPlayer implements OnPreparedListener {
+        AudioPreview mActivity;
+        boolean mIsPrepared = false;
+
+        public void setActivity(AudioPreview activity) {
+            mActivity = activity;
+            setOnPreparedListener(this);
+            setOnErrorListener(mActivity);
+            setOnCompletionListener(mActivity);
+        }
+
+        public void setDataSourceAndPrepare(Uri uri) throws IllegalArgumentException,
+                                                            SecurityException,
+                                                            IllegalStateException, IOException {
+            setDataSource(mActivity, uri);
+            prepareAsync();
+        }
+
+        /* (non-Javadoc)
+         * @see android.media.MediaPlayer.OnPreparedListener#onPrepared(android.media.MediaPlayer)
+         */
+        @Override
+        public void onPrepared(MediaPlayer mp) {
+            mIsPrepared = true;
+            mActivity.onPrepared(mp);
+        }
+
+        boolean isPrepared() {
+            return mIsPrepared;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/music/MediaPlaybackActivity.java b/src/com/android/music/MediaPlaybackActivity.java
index 099ef4c..6b83073 100644
--- a/src/com/android/music/MediaPlaybackActivity.java
+++ b/src/com/android/music/MediaPlaybackActivity.java
@@ -42,8 +42,15 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.Window;
-import android.widget.*;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
 import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+import android.widget.Toast;
+
 import com.android.music.utils.LogHelper;
 import com.android.music.utils.MediaIDHelper;
 import com.android.music.utils.MusicProvider;
@@ -656,11 +663,11 @@
 
     private long updateProgressBar() {
         MediaController mediaController = getMediaController();
-        if (mediaController == null) {
+        if (mediaController == null || mediaController.getMetadata() == null
+                || mediaController.getPlaybackState() == null) {
             return 500;
         }
-        long duration =
-                getMediaController().getMetadata().getLong(MediaMetadata.METADATA_KEY_DURATION);
+        long duration = mediaController.getMetadata().getLong(MediaMetadata.METADATA_KEY_DURATION);
         long pos = mediaController.getPlaybackState().getPosition();
         if ((pos >= 0) && (duration > 0)) {
             mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000));
diff --git a/src/com/android/music/MusicPicker.java b/src/com/android/music/MusicPicker.java
new file mode 100644
index 0000000..ea32ec9
--- /dev/null
+++ b/src/com/android/music/MusicPicker.java
@@ -0,0 +1,688 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.music;
+
+import android.app.ListActivity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.RadioButton;
+import android.widget.SectionIndexer;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.text.Collator;
+import java.util.Formatter;
+import java.util.Locale;
+
+/**
+ * Activity allowing the user to select a music track on the device, and
+ * return it to its caller.  The music picker user interface is fairly
+ * extensive, providing information about each track like the music
+ * application (title, author, album, duration), as well as the ability to
+ * previous tracks and sort them in different orders.
+ *
+ * <p>This class also illustrates how you can load data from a content
+ * provider asynchronously, providing a good UI while doing so, perform
+ * indexing of the content for use inside of a {@link FastScrollView}, and
+ * perform filtering of the data as the user presses keys.
+ */
+public class MusicPicker
+        extends ListActivity implements View.OnClickListener, MediaPlayer.OnCompletionListener {
+    static final boolean DBG = false;
+    static final String TAG = "MusicPicker";
+
+    /** Holds the previous state of the list, to restore after the async
+     * query has completed. */
+    static final String LIST_STATE_KEY = "liststate";
+    /** Remember whether the list last had focus for restoring its state. */
+    static final String FOCUS_KEY = "focused";
+    /** Remember the last ordering mode for restoring state. */
+    static final String SORT_MODE_KEY = "sortMode";
+
+    /** Arbitrary number, doesn't matter since we only do one query type. */
+    static final int MY_QUERY_TOKEN = 42;
+
+    /** Menu item to sort the music list by track title. */
+    static final int TRACK_MENU = Menu.FIRST;
+    /** Menu item to sort the music list by album title. */
+    static final int ALBUM_MENU = Menu.FIRST + 1;
+    /** Menu item to sort the music list by artist name. */
+    static final int ARTIST_MENU = Menu.FIRST + 2;
+
+    /** These are the columns in the music cursor that we are interested in. */
+    static final String[] CURSOR_COLS = new String[] {MediaStore.Audio.Media._ID,
+            MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.TITLE_KEY,
+            MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM,
+            MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ARTIST_ID,
+            MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.TRACK};
+
+    /** Formatting optimization to avoid creating many temporary objects. */
+    static StringBuilder sFormatBuilder = new StringBuilder();
+    /** Formatting optimization to avoid creating many temporary objects. */
+    static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
+    /** Formatting optimization to avoid creating many temporary objects. */
+    static final Object[] sTimeArgs = new Object[5];
+
+    /** Uri to the directory of all music being displayed. */
+    Uri mBaseUri;
+
+    /** This is the adapter used to display all of the tracks. */
+    TrackListAdapter mAdapter;
+    /** Our instance of QueryHandler used to perform async background queries. */
+    QueryHandler mQueryHandler;
+
+    /** Used to keep track of the last scroll state of the list. */
+    Parcelable mListState = null;
+    /** Used to keep track of whether the list last had focus. */
+    boolean mListHasFocus;
+
+    /** The current cursor on the music that is being displayed. */
+    Cursor mCursor;
+    /** The actual sort order the user has selected. */
+    int mSortMode = -1;
+    /** SQL order by string describing the currently selected sort order. */
+    String mSortOrder;
+
+    /** Container of the in-screen progress indicator, to be able to hide it
+     * when done loading the initial cursor. */
+    View mProgressContainer;
+    /** Container of the list view hierarchy, to be able to show it when done
+     * loading the initial cursor. */
+    View mListContainer;
+    /** Set to true when the list view has been shown for the first time. */
+    boolean mListShown;
+
+    /** View holding the okay button. */
+    View mOkayButton;
+    /** View holding the cancel button. */
+    View mCancelButton;
+
+    /** Which track row ID the user has last selected. */
+    long mSelectedId = -1;
+    /** Completel Uri that the user has last selected. */
+    Uri mSelectedUri;
+
+    /** If >= 0, we are currently playing a track for preview, and this is its
+     * row ID. */
+    long mPlayingId = -1;
+
+    /** This is used for playing previews of the music files. */
+    MediaPlayer mMediaPlayer;
+
+    /**
+     * A special implementation of SimpleCursorAdapter that knows how to bind
+     * our cursor data to our list item structure, and takes care of other
+     * advanced features such as indexing and filtering.
+     */
+    class TrackListAdapter extends SimpleCursorAdapter implements SectionIndexer {
+        final ListView mListView;
+
+        private final StringBuilder mBuilder = new StringBuilder();
+        private final String mUnknownArtist;
+        private final String mUnknownAlbum;
+
+        private int mIdIdx;
+        private int mTitleIdx;
+        private int mArtistIdx;
+        private int mAlbumIdx;
+        private int mDurationIdx;
+
+        private boolean mLoading = true;
+        private int mIndexerSortMode;
+        private MusicAlphabetIndexer mIndexer;
+
+        class ViewHolder {
+            TextView line1;
+            TextView line2;
+            TextView duration;
+            RadioButton radio;
+            ImageView play_indicator;
+            CharArrayBuffer buffer1;
+            char[] buffer2;
+        }
+
+        TrackListAdapter(Context context, ListView listView, int layout, String[] from, int[] to) {
+            super(context, layout, null, from, to);
+            mListView = listView;
+            mUnknownArtist = context.getString(R.string.unknown_artist_name);
+            mUnknownAlbum = context.getString(R.string.unknown_album_name);
+        }
+
+        /**
+         * The mLoading flag is set while we are performing a background
+         * query, to avoid displaying the "No music" empty view during
+         * this time.
+         */
+        public void setLoading(boolean loading) {
+            mLoading = loading;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            if (mLoading) {
+                // We don't want the empty state to show when loading.
+                return false;
+            } else {
+                return super.isEmpty();
+            }
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            View v = super.newView(context, cursor, parent);
+            ViewHolder vh = new ViewHolder();
+            vh.line1 = (TextView) v.findViewById(R.id.line1);
+            vh.line2 = (TextView) v.findViewById(R.id.line2);
+            vh.duration = (TextView) v.findViewById(R.id.duration);
+            vh.radio = (RadioButton) v.findViewById(R.id.radio);
+            vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
+            vh.buffer1 = new CharArrayBuffer(100);
+            vh.buffer2 = new char[200];
+            v.setTag(vh);
+            return v;
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            ViewHolder vh = (ViewHolder) view.getTag();
+
+            cursor.copyStringToBuffer(mTitleIdx, vh.buffer1);
+            vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied);
+
+            int secs = cursor.getInt(mDurationIdx) / 1000;
+            if (secs == 0) {
+                vh.duration.setText("");
+            } else {
+                vh.duration.setText(MusicUtils.makeTimeString(context, secs));
+            }
+
+            final StringBuilder builder = mBuilder;
+            builder.delete(0, builder.length());
+
+            String name = cursor.getString(mAlbumIdx);
+            if (name == null || name.equals("<unknown>")) {
+                builder.append(mUnknownAlbum);
+            } else {
+                builder.append(name);
+            }
+            builder.append('\n');
+            name = cursor.getString(mArtistIdx);
+            if (name == null || name.equals("<unknown>")) {
+                builder.append(mUnknownArtist);
+            } else {
+                builder.append(name);
+            }
+            int len = builder.length();
+            if (vh.buffer2.length < len) {
+                vh.buffer2 = new char[len];
+            }
+            builder.getChars(0, len, vh.buffer2, 0);
+            vh.line2.setText(vh.buffer2, 0, len);
+
+            // Update the checkbox of the item, based on which the user last
+            // selected.  Note that doing it this way means we must have the
+            // list view update all of its items when the selected item
+            // changes.
+            final long id = cursor.getLong(mIdIdx);
+            vh.radio.setChecked(id == mSelectedId);
+            if (DBG)
+                Log.v(TAG,
+                        "Binding id=" + id + " sel=" + mSelectedId + " playing=" + mPlayingId
+                                + " cursor=" + cursor);
+
+            // Likewise, display the "now playing" icon if this item is
+            // currently being previewed for the user.
+            ImageView iv = vh.play_indicator;
+            if (id == mPlayingId) {
+                iv.setImageResource(R.drawable.indicator_ic_mp_playing_list);
+                iv.setVisibility(View.VISIBLE);
+            } else {
+                iv.setVisibility(View.GONE);
+            }
+        }
+
+        /**
+         * This method is called whenever we receive a new cursor due to
+         * an async query, and must take care of plugging the new one in
+         * to the adapter.
+         */
+        @Override
+        public void changeCursor(Cursor cursor) {
+            super.changeCursor(cursor);
+            if (DBG)
+                Log.v(TAG, "Setting cursor to: " + cursor + " from: " + MusicPicker.this.mCursor);
+
+            MusicPicker.this.mCursor = cursor;
+
+            if (cursor != null) {
+                // Retrieve indices of the various columns we are interested in.
+                mIdIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
+                mTitleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
+                mArtistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
+                mAlbumIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM);
+                mDurationIdx = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
+
+                // If the sort mode has changed, or we haven't yet created an
+                // indexer one, then create a new one that is indexing the
+                // appropriate column based on the sort mode.
+                if (mIndexerSortMode != mSortMode || mIndexer == null) {
+                    mIndexerSortMode = mSortMode;
+                    int idx = mTitleIdx;
+                    switch (mIndexerSortMode) {
+                        case ARTIST_MENU:
+                            idx = mArtistIdx;
+                            break;
+                        case ALBUM_MENU:
+                            idx = mAlbumIdx;
+                            break;
+                    }
+                    mIndexer = new MusicAlphabetIndexer(
+                            cursor, idx, getResources().getString(R.string.fast_scroll_alphabet));
+
+                    // If we have a valid indexer, but the cursor has changed since
+                    // its last use, then point it to the current cursor.
+                } else {
+                    mIndexer.setCursor(cursor);
+                }
+            }
+
+            // Ensure that the list is shown (and initial progress indicator
+            // hidden) in case this is the first cursor we have gotten.
+            makeListShown();
+        }
+
+        /**
+         * This method is called from a background thread by the list view
+         * when the user has typed a letter that should result in a filtering
+         * of the displayed items.  It returns a Cursor, when will then be
+         * handed to changeCursor.
+         */
+        @Override
+        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+            if (DBG) Log.v(TAG, "Getting new cursor...");
+            return doQuery(true, constraint.toString());
+        }
+
+        public int getPositionForSection(int section) {
+            Cursor cursor = getCursor();
+            if (cursor == null) {
+                // No cursor, the section doesn't exist so just return 0
+                return 0;
+            }
+
+            return mIndexer.getPositionForSection(section);
+        }
+
+        public int getSectionForPosition(int position) {
+            return 0;
+        }
+
+        public Object[] getSections() {
+            if (mIndexer != null) {
+                return mIndexer.getSections();
+            }
+            return null;
+        }
+    }
+
+    /**
+     * This is our specialization of AsyncQueryHandler applies new cursors
+     * to our state as they become available.
+     */
+    private final class QueryHandler extends AsyncQueryHandler {
+        public QueryHandler(Context context) {
+            super(context.getContentResolver());
+        }
+
+        @Override
+        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+            if (!isFinishing()) {
+                // Update the adapter: we are no longer loading, and have
+                // a new cursor for it.
+                mAdapter.setLoading(false);
+                mAdapter.changeCursor(cursor);
+                setProgressBarIndeterminateVisibility(false);
+
+                // Now that the cursor is populated again, it's possible to restore the list state
+                if (mListState != null) {
+                    getListView().onRestoreInstanceState(mListState);
+                    if (mListHasFocus) {
+                        getListView().requestFocus();
+                    }
+                    mListHasFocus = false;
+                    mListState = null;
+                }
+            } else {
+                cursor.close();
+            }
+        }
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+        int sortMode = TRACK_MENU;
+        if (icicle == null) {
+            mSelectedUri =
+                    getIntent().getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
+        } else {
+            mSelectedUri = (Uri) icicle.getParcelable(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
+            // Retrieve list state. This will be applied after the
+            // QueryHandler has run
+            mListState = icicle.getParcelable(LIST_STATE_KEY);
+            mListHasFocus = icicle.getBoolean(FOCUS_KEY);
+            sortMode = icicle.getInt(SORT_MODE_KEY, sortMode);
+        }
+        if (Intent.ACTION_GET_CONTENT.equals(getIntent().getAction())) {
+            mBaseUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+        } else {
+            mBaseUri = getIntent().getData();
+            if (mBaseUri == null) {
+                Log.w("MusicPicker", "No data URI given to PICK action");
+                finish();
+                return;
+            }
+        }
+
+        setContentView(R.layout.music_picker);
+
+        mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
+
+        final ListView listView = getListView();
+
+        listView.setItemsCanFocus(false);
+
+        mAdapter = new TrackListAdapter(
+                this, listView, R.layout.music_picker_item, new String[] {}, new int[] {});
+
+        setListAdapter(mAdapter);
+
+        listView.setTextFilterEnabled(true);
+
+        // We manually save/restore the listview state
+        listView.setSaveEnabled(false);
+
+        mQueryHandler = new QueryHandler(this);
+
+        mProgressContainer = findViewById(R.id.progressContainer);
+        mListContainer = findViewById(R.id.listContainer);
+
+        mOkayButton = findViewById(R.id.okayButton);
+        mOkayButton.setOnClickListener(this);
+        mCancelButton = findViewById(R.id.cancelButton);
+        mCancelButton.setOnClickListener(this);
+
+        // If there is a currently selected Uri, then try to determine who
+        // it is.
+        if (mSelectedUri != null) {
+            Uri.Builder builder = mSelectedUri.buildUpon();
+            String path = mSelectedUri.getEncodedPath();
+            int idx = path.lastIndexOf('/');
+            if (idx >= 0) {
+                path = path.substring(0, idx);
+            }
+            builder.encodedPath(path);
+            Uri baseSelectedUri = builder.build();
+            if (DBG) Log.v(TAG, "Selected Uri: " + mSelectedUri);
+            if (DBG) Log.v(TAG, "Selected base Uri: " + baseSelectedUri);
+            if (DBG) Log.v(TAG, "Base Uri: " + mBaseUri);
+            if (baseSelectedUri.equals(mBaseUri)) {
+                // If the base Uri of the selected Uri is the same as our
+                // content's base Uri, then use the selection!
+                mSelectedId = ContentUris.parseId(mSelectedUri);
+            }
+        }
+
+        setSortMode(sortMode);
+    }
+
+    @Override
+    public void onRestart() {
+        super.onRestart();
+        doQuery(false, null);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (setSortMode(item.getItemId())) {
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        menu.add(Menu.NONE, TRACK_MENU, Menu.NONE, R.string.sort_by_track);
+        menu.add(Menu.NONE, ALBUM_MENU, Menu.NONE, R.string.sort_by_album);
+        menu.add(Menu.NONE, ARTIST_MENU, Menu.NONE, R.string.sort_by_artist);
+        return true;
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle icicle) {
+        super.onSaveInstanceState(icicle);
+        // Save list state in the bundle so we can restore it after the
+        // QueryHandler has run
+        icicle.putParcelable(LIST_STATE_KEY, getListView().onSaveInstanceState());
+        icicle.putBoolean(FOCUS_KEY, getListView().hasFocus());
+        icicle.putInt(SORT_MODE_KEY, mSortMode);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        stopMediaPlayer();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        // We don't want the list to display the empty state, since when we
+        // resume it will still be there and show up while the new query is
+        // happening. After the async query finishes in response to onResume()
+        // setLoading(false) will be called.
+        mAdapter.setLoading(true);
+        mAdapter.changeCursor(null);
+    }
+
+    /**
+     * Changes the current sort order, building the appropriate query string
+     * for the selected order.
+     */
+    boolean setSortMode(int sortMode) {
+        if (sortMode != mSortMode) {
+            switch (sortMode) {
+                case TRACK_MENU:
+                    mSortMode = sortMode;
+                    mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
+                    doQuery(false, null);
+                    return true;
+                case ALBUM_MENU:
+                    mSortMode = sortMode;
+                    mSortOrder = MediaStore.Audio.Media.ALBUM_KEY + " ASC, "
+                            + MediaStore.Audio.Media.TRACK + " ASC, "
+                            + MediaStore.Audio.Media.TITLE_KEY + " ASC";
+                    doQuery(false, null);
+                    return true;
+                case ARTIST_MENU:
+                    mSortMode = sortMode;
+                    mSortOrder = MediaStore.Audio.Media.ARTIST_KEY + " ASC, "
+                            + MediaStore.Audio.Media.ALBUM_KEY + " ASC, "
+                            + MediaStore.Audio.Media.TRACK + " ASC, "
+                            + MediaStore.Audio.Media.TITLE_KEY + " ASC";
+                    doQuery(false, null);
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * The first time this is called, we hide the large progress indicator
+     * and show the list view, doing fade animations between them.
+     */
+    void makeListShown() {
+        if (!mListShown) {
+            mListShown = true;
+            mProgressContainer.startAnimation(
+                    AnimationUtils.loadAnimation(this, android.R.anim.fade_out));
+            mProgressContainer.setVisibility(View.GONE);
+            mListContainer.startAnimation(
+                    AnimationUtils.loadAnimation(this, android.R.anim.fade_in));
+            mListContainer.setVisibility(View.VISIBLE);
+        }
+    }
+
+    /**
+     * Common method for performing a query of the music database, called for
+     * both top-level queries and filtering.
+     *
+     * @param sync If true, this query should be done synchronously and the
+     * resulting cursor returned.  If false, it will be done asynchronously and
+     * null returned.
+     * @param filterstring If non-null, this is a filter to apply to the query.
+     */
+    Cursor doQuery(boolean sync, String filterstring) {
+        // Cancel any pending queries
+        mQueryHandler.cancelOperation(MY_QUERY_TOKEN);
+
+        StringBuilder where = new StringBuilder();
+        where.append(MediaStore.Audio.Media.TITLE + " != ''");
+
+        // We want to show all audio files, even recordings.  Enforcing the
+        // following condition would hide recordings.
+        // where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
+
+        Uri uri = mBaseUri;
+        if (!TextUtils.isEmpty(filterstring)) {
+            uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filterstring)).build();
+        }
+
+        if (sync) {
+            try {
+                return getContentResolver().query(
+                        uri, CURSOR_COLS, where.toString(), null, mSortOrder);
+            } catch (UnsupportedOperationException ex) {
+            }
+        } else {
+            mAdapter.setLoading(true);
+            setProgressBarIndeterminateVisibility(true);
+            mQueryHandler.startQuery(
+                    MY_QUERY_TOKEN, null, uri, CURSOR_COLS, where.toString(), null, mSortOrder);
+        }
+        return null;
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id) {
+        mCursor.moveToPosition(position);
+        if (DBG)
+            Log.v(TAG,
+                    "Click on " + position + " (id=" + id + ", cursid="
+                            + mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID))
+                            + ") in cursor " + mCursor + " adapter=" + l.getAdapter());
+        setSelected(mCursor);
+    }
+
+    void setSelected(Cursor c) {
+        Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+        long newId = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID));
+        mSelectedUri = ContentUris.withAppendedId(uri, newId);
+
+        mSelectedId = newId;
+        if (newId != mPlayingId || mMediaPlayer == null) {
+            stopMediaPlayer();
+            mMediaPlayer = new MediaPlayer();
+            try {
+                mMediaPlayer.setDataSource(this, mSelectedUri);
+                mMediaPlayer.setOnCompletionListener(this);
+                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
+                mMediaPlayer.prepare();
+                mMediaPlayer.start();
+                mPlayingId = newId;
+                getListView().invalidateViews();
+            } catch (IOException e) {
+                Log.w("MusicPicker", "Unable to play track", e);
+            }
+        } else if (mMediaPlayer != null) {
+            stopMediaPlayer();
+            getListView().invalidateViews();
+        }
+    }
+
+    public void onCompletion(MediaPlayer mp) {
+        if (mMediaPlayer == mp) {
+            mp.stop();
+            mp.release();
+            mMediaPlayer = null;
+            mPlayingId = -1;
+            getListView().invalidateViews();
+        }
+    }
+
+    void stopMediaPlayer() {
+        if (mMediaPlayer != null) {
+            mMediaPlayer.stop();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+            mPlayingId = -1;
+        }
+    }
+
+    public void onClick(View v) {
+        switch (v.getId()) {
+            case R.id.okayButton:
+                if (mSelectedId >= 0) {
+                    setResult(RESULT_OK, new Intent().setData(mSelectedUri));
+                    finish();
+                }
+                break;
+
+            case R.id.cancelButton:
+                finish();
+                break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/music/TrackBrowserActivity.java b/src/com/android/music/TrackBrowserActivity.java
index 69ce726..7fb41b4 100644
--- a/src/com/android/music/TrackBrowserActivity.java
+++ b/src/com/android/music/TrackBrowserActivity.java
@@ -82,7 +82,7 @@
             MusicUtils.updateNowPlaying(this);
         } else if (intent != null) {
             LogHelper.d(TAG, "Launch by intent");
-            mParentItem = intent.getExtras().getParcelable(MusicUtils.TAG_PARENT_ITEM);
+            mParentItem = intent.getParcelableExtra(MusicUtils.TAG_PARENT_ITEM);
             mWithTabs = intent.getBooleanExtra(MusicUtils.TAG_WITH_TABS, false);
         }
         if (mParentItem == null) {