Gallery2: support audio effects during video play.

Porting from kk with fixes

Conflicts:
	res/values/strings.xml

Change-Id: I267c3c7e402fd5cf2bb223547003af491dd8289e
Signed-off-by: Xiaojing Zhang <zhangx@codeaurora.org>
diff --git a/res/drawable-hdpi/knob.png b/res/drawable-hdpi/knob.png
new file mode 100644
index 0000000..a6871ad
--- /dev/null
+++ b/res/drawable-hdpi/knob.png
Binary files differ
diff --git a/res/drawable-hdpi/knob_bg.png b/res/drawable-hdpi/knob_bg.png
new file mode 100644
index 0000000..3423509
--- /dev/null
+++ b/res/drawable-hdpi/knob_bg.png
Binary files differ
diff --git a/res/drawable-hdpi/knob_toggle_off.png b/res/drawable-hdpi/knob_toggle_off.png
new file mode 100644
index 0000000..a89595e
--- /dev/null
+++ b/res/drawable-hdpi/knob_toggle_off.png
Binary files differ
diff --git a/res/drawable-hdpi/knob_toggle_on.png b/res/drawable-hdpi/knob_toggle_on.png
new file mode 100644
index 0000000..ab8d3d7
--- /dev/null
+++ b/res/drawable-hdpi/knob_toggle_on.png
Binary files differ
diff --git a/res/layout/audio_effects_dialog.xml b/res/layout/audio_effects_dialog.xml
new file mode 100644
index 0000000..83892c2
--- /dev/null
+++ b/res/layout/audio_effects_dialog.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (c) 2013, The Linux Foundation. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of The Linux Foundation nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:custom="http://schemas.android.com/apk/res/com.android.gallery3d"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:gravity="center">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:gravity="center_horizontal"
+        android:paddingBottom="10dp">
+
+        <Switch
+            android:id="@+id/audio_effects_switch"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/audio_effects"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:layout_marginTop="10dp"
+            android:layout_marginLeft="25dp"
+            android:layout_marginRight="30dp" />
+
+        <LinearLayout
+            android:id="@+id/aEffectsPanel"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginTop="5dp"
+            android:layout_marginBottom="10dp"
+            android:gravity="center_vertical">
+
+            <com.android.gallery3d.ui.Knob
+                android:id="@+id/bBStrengthKnob"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                custom:label="@string/bass_boost_strength"
+                custom:background="@drawable/knob_bg"
+                custom:foreground="@drawable/knob" />
+
+            <com.android.gallery3d.ui.Knob
+                android:id="@+id/vIStrengthKnob"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                custom:label="@string/virtualizer_strength"
+                custom:background="@drawable/knob_bg"
+                custom:foreground="@drawable/knob" />
+
+        </LinearLayout>
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/knob.xml b/res/layout/knob.xml
new file mode 100644
index 0000000..41c982e
--- /dev/null
+++ b/res/layout/knob.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (c) 2013, The Linux Foundation. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of The Linux Foundation nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <ImageView
+        android:id="@+id/knob_foreground"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+    <ImageView
+        android:id="@+id/knob_toggle_on"
+        android:layout_gravity="center_horizontal|center_vertical"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/knob_toggle_on"
+        android:visibility="gone" />
+    <ImageView
+        android:id="@+id/knob_toggle_off"
+        android:layout_gravity="center_horizontal|center_vertical"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/knob_toggle_off"
+        android:visibility="gone" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center_horizontal"
+        android:orientation="vertical"
+        android:gravity="center_horizontal">
+
+        <TextView
+            android:id="@+id/knob_value"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:visibility="gone" />
+        <TextView
+            android:id="@+id/knob_label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="marquee"
+            android:visibility="gone" />
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 91a6358..91dc8ef 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -436,4 +436,8 @@
     <item quantity="one" msgid="6949174783125614798">"%1$d张照片"</item>
     <item quantity="other" msgid="3813306834113858135">"%1$d张照片"</item>
   </plurals>
+
+    <string name="bass_boost_strength" msgid="882301530007752270">"低音增强"</string>
+    <string name="virtualizer_strength" msgid="5035111173763913313">"3D 音效"</string>
+    <string name="audio_effects" msgid="612896145300512593">"音效"</string>
 </resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 88fa970..edb7f69 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -436,4 +436,8 @@
     <item quantity="one" msgid="6949174783125614798">"%1$d 張相片"</item>
     <item quantity="other" msgid="3813306834113858135">"%1$d 張相片"</item>
   </plurals>
+
+    <string name="bass_boost_strength">"低音加強"</string>
+    <string name="virtualizer_strength">"3D 效果"</string>
+    <string name="audio_effects">"音效"</string>
 </resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 5a00a69..eadd4f2 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -42,5 +42,9 @@
         <attr name="largeIcons" format="reference" />
         <attr name="images" format="reference" />
     </declare-styleable>
-
+    <declare-styleable name="Knob">
+        <attr name="label" format="string" />
+        <attr name="background" format="integer" />
+        <attr name="foreground" format="integer" />
+    </declare-styleable>
 </resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 4fe9180..1f1d6ec 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -70,4 +70,8 @@
     <color name="face_detect_fail">#80d05060</color>
     <color name="gray">#FFAAAAAA</color>
 
+    <color name="highlight">#00a8ff</color>
+    <color name="lowlight">#00527c</color>
+    <color name="grey">#999999</color>
+    <color name="disabled_knob">#505050</color>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e19f950..bb9bf18 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1119,6 +1119,21 @@
         <item quantity="other">%1$d photos</item>
     </plurals>
 
+    <!-- The label for the bass boost knob of the audio effects dialog. -->
+    <string name="bass_boost_strength">Bass boost</string>
+
+    <!-- The label for the 3d effect knob of the audio effects dialog. -->
+    <string name="virtualizer_strength">3D effect</string>
+
+    <!-- The label for the audio effects menu. -->
+    <string name="audio_effects">Audio effects</string>
+
+    <!-- Toast if user attempts to control audio effects without headphones plugged in. -->
+    <string name="headset_plug">Plug in headphones for these effects.</string>
+
+    <!-- The title of the audio effects dialog. -->
+    <string name="audio_effects_dialog_title">Snapdragon Audio+</string>
+
     <!-- The tips of trimming video -->
     <string name="fail_trim">Sorry, this video file can not be trimmed</string>
 </resources>
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
index 1547f6f..30b1224 100644
--- a/src/com/android/gallery3d/app/MovieActivity.java
+++ b/src/com/android/gallery3d/app/MovieActivity.java
@@ -19,30 +19,49 @@
 import android.annotation.TargetApi;
 import android.app.ActionBar;
 import android.app.Activity;
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
 import android.content.AsyncQueryHandler;
+import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.content.pm.ActivityInfo;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.media.AudioManager;
+import android.media.audiofx.AudioEffect;
+import android.media.audiofx.AudioEffect.Descriptor;
+import android.media.audiofx.BassBoost;
+import android.media.audiofx.Virtualizer;
+import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.provider.MediaStore;
 import android.provider.OpenableColumns;
+import android.view.Gravity;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
+import android.widget.CompoundButton;
 import android.widget.ShareActionProvider;
+import android.widget.Switch;
+import android.widget.Toast;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.Knob;
 
 /**
  * This activity plays a video from a specified URI.
@@ -62,6 +81,56 @@
     private Uri mUri;
     private boolean mTreatUpAsBack;
 
+    private static final short BASSBOOST_MAX_STRENGTH = 1000;
+    private static final short VIRTUALIZER_MAX_STRENGTH = 1000;
+
+    private boolean mIsHeadsetOn = false;
+    private boolean mVirtualizerSupported = false;
+    private boolean mBassBoostSupported = false;
+
+    private SharedPreferences mPrefs;
+    static enum Key {
+        global_enabled, bb_strength, virt_strength
+    };
+
+    private BassBoost mBassBoostEffect;
+    private Virtualizer mVirtualizerEffect;
+    private AlertDialog mEffectDialog;
+    private Switch mSwitch;
+    private Knob mBassBoostKnob;
+    private Knob mVirtualizerKnob;
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(final Context context, final Intent intent) {
+            final String action = intent.getAction();
+            final AudioManager audioManager =
+                (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+            if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
+                mIsHeadsetOn = (intent.getIntExtra("state", 0) == 1)
+                        || audioManager.isBluetoothA2dpOn();
+            } else if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)
+                    || action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
+                final int deviceClass = ((BluetoothDevice)
+                        intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE))
+                        .getBluetoothClass().getDeviceClass();
+                if ((deviceClass == BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES)
+                        || (deviceClass == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)) {
+                    mIsHeadsetOn = action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)
+                            || audioManager.isWiredHeadsetOn();
+                }
+            } else if (action.equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
+                mIsHeadsetOn = audioManager.isBluetoothA2dpOn() || audioManager.isWiredHeadsetOn();
+            }
+            if (mEffectDialog != null) {
+                if (!mIsHeadsetOn && mEffectDialog.isShowing()) {
+                    mEffectDialog.dismiss();
+                    showHeadsetPlugToast();
+                }
+            }
+        }
+    };
+
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     private void setSystemUiVisibility(View rootView) {
         if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
@@ -114,6 +183,46 @@
         // We set the background in the theme to have the launching animation.
         // But for the performance (and battery), we remove the background here.
         win.setBackgroundDrawable(null);
+
+        // Determine available/supported effects
+        final Descriptor[] effects = AudioEffect.queryEffects();
+        for (final Descriptor effect : effects) {
+            if (effect.type.equals(AudioEffect.EFFECT_TYPE_VIRTUALIZER)) {
+                mVirtualizerSupported = true;
+            } else if (effect.type.equals(AudioEffect.EFFECT_TYPE_BASS_BOOST)) {
+                mBassBoostSupported = true;
+            }
+        }
+
+        mPrefs = getSharedPreferences(getApplicationContext().getPackageName(),
+                Context.MODE_PRIVATE);
+
+        mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+            @Override
+            public void onPrepared(MediaPlayer mp) {
+                int sessionId = mp.getAudioSessionId();
+                if (mBassBoostSupported) {
+                    mBassBoostEffect = new BassBoost(0, sessionId);
+                }
+                if (mVirtualizerSupported) {
+                    mVirtualizerEffect = new Virtualizer(0, sessionId);
+                }
+                if (mIsHeadsetOn) {
+                    if (mPrefs.getBoolean(Key.global_enabled.toString(), false)) {
+                        if (mBassBoostSupported) {
+                            mBassBoostEffect.setStrength((short)
+                                    mPrefs.getInt(Key.bb_strength.toString(), 0));
+                            mBassBoostEffect.setEnabled(true);
+                        }
+                        if (mVirtualizerSupported) {
+                            mVirtualizerEffect.setStrength((short)
+                                mPrefs.getInt(Key.virt_strength.toString(), 0));
+                            mVirtualizerEffect.setEnabled(true);
+                        }
+                    }
+                }
+            }
+        });
     }
 
     private void setActionBarLogoFromIntent(Intent intent) {
@@ -181,9 +290,107 @@
         } else {
             shareItem.setVisible(false);
         }
+
+        final MenuItem mi = menu.add(R.string.audio_effects);
+        mi.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                onAudioEffectsMenuItemClick();
+                return true;
+            }
+        });
         return true;
     }
 
+    private void onAudioEffectsMenuItemClick() {
+        if (!mIsHeadsetOn) {
+            showHeadsetPlugToast();
+        } else {
+            LayoutInflater factory = LayoutInflater.from(this);
+            final View content = factory.inflate(R.layout.audio_effects_dialog, null);
+
+            boolean enabled = mPrefs.getBoolean(Key.global_enabled.toString(), false);
+
+            mSwitch = (Switch) content.findViewById(R.id.audio_effects_switch);
+            mSwitch.setChecked(enabled);
+            mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+                @Override
+                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                    mBassBoostEffect.setEnabled(isChecked);
+                    mBassBoostEffect.setStrength((short)
+                            mPrefs.getInt(Key.bb_strength.toString(), 0));
+                    mVirtualizerEffect.setEnabled(isChecked);
+                    mVirtualizerEffect.setStrength((short)
+                        mPrefs.getInt(Key.virt_strength.toString(), 0));
+                    mBassBoostKnob.setEnabled(isChecked);
+                    mVirtualizerKnob.setEnabled(isChecked);
+                }
+            });
+
+            mBassBoostKnob = (Knob) content.findViewById(R.id.bBStrengthKnob);
+            mBassBoostKnob.setEnabled(enabled);
+            mBassBoostKnob.setMax(BASSBOOST_MAX_STRENGTH);
+            mBassBoostKnob.setValue(mPrefs.getInt(Key.bb_strength.toString(), 0));
+            mBassBoostKnob.setOnKnobChangeListener(new Knob.OnKnobChangeListener() {
+                @Override
+                public void onValueChanged(Knob knob, int value, boolean fromUser) {
+                    mBassBoostEffect.setStrength((short) value);
+                }
+
+                @Override
+                public boolean onSwitchChanged(Knob knob, boolean enabled) {
+                    return false;
+                }
+            });
+
+            mVirtualizerKnob = (Knob) content.findViewById(R.id.vIStrengthKnob);
+            mVirtualizerKnob.setEnabled(enabled);
+            mVirtualizerKnob.setMax(VIRTUALIZER_MAX_STRENGTH);
+            mVirtualizerKnob.setValue(mPrefs.getInt(Key.virt_strength.toString(), 0));
+            mVirtualizerKnob.setOnKnobChangeListener(new Knob.OnKnobChangeListener() {
+                @Override
+                public void onValueChanged(Knob knob, int value, boolean fromUser) {
+                    mVirtualizerEffect.setStrength((short) value);
+                }
+
+                @Override
+                public boolean onSwitchChanged(Knob knob, boolean enabled) {
+                    return false;
+                }
+            });
+
+            mEffectDialog = new AlertDialog.Builder(MovieActivity.this,
+                    AlertDialog.THEME_HOLO_DARK)
+                .setTitle(R.string.audio_effects_dialog_title)
+                .setView(content)
+                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        SharedPreferences.Editor editor = mPrefs.edit();
+                        editor.putBoolean(Key.global_enabled.toString(), mSwitch.isChecked());
+                        editor.putInt(Key.bb_strength.toString(), mBassBoostKnob.getValue());
+                        editor.putInt(Key.virt_strength.toString(),
+                                mVirtualizerKnob.getValue());
+                        editor.commit();
+                    }
+                })
+                .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        boolean enabled = mPrefs.getBoolean(Key.global_enabled.toString(), false);
+                        mBassBoostEffect.setStrength((short)
+                                mPrefs.getInt(Key.bb_strength.toString(), 0));
+                        mBassBoostEffect.setEnabled(enabled);
+                        mVirtualizerEffect.setStrength((short)
+                            mPrefs.getInt(Key.virt_strength.toString(), 0));
+                        mVirtualizerEffect.setEnabled(enabled);
+                    }
+                })
+                .create();
+            mEffectDialog.show();
+        }
+    }
+
     private Intent createShareIntent() {
         Intent intent = new Intent(Intent.ACTION_SEND);
         intent.setType("video/*");
@@ -210,6 +417,13 @@
         return false;
     }
 
+    public void showHeadsetPlugToast() {
+        final Toast toast = Toast.makeText(getApplicationContext(), R.string.headset_plug,
+                Toast.LENGTH_LONG);
+        toast.setGravity(Gravity.CENTER, toast.getXOffset() / 2, toast.getYOffset() / 2);
+        toast.show();
+    }
+
     @Override
     public void onStart() {
         ((AudioManager) getSystemService(AUDIO_SERVICE))
@@ -228,12 +442,24 @@
     @Override
     public void onPause() {
         mPlayer.onPause();
+        try {
+            unregisterReceiver(mReceiver);
+        } catch (IllegalArgumentException e) {
+            // Do nothing
+        }
         super.onPause();
     }
 
     @Override
     public void onResume() {
         mPlayer.onResume();
+        if ((mVirtualizerSupported) || (mBassBoostSupported)) {
+            final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+            intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
+            intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
+            intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+            registerReceiver(mReceiver, intentFilter);
+        }
         super.onResume();
     }
 
@@ -246,6 +472,14 @@
     @Override
     public void onDestroy() {
         mPlayer.onDestroy();
+        if (mBassBoostEffect != null) {
+            mBassBoostEffect.setEnabled(false);
+            mBassBoostEffect.release();
+        }
+        if (mVirtualizerEffect != null) {
+            mVirtualizerEffect.setEnabled(false);
+            mVirtualizerEffect.release();
+        }
         super.onDestroy();
     }
 
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
index f6bd367..962afea 100644
--- a/src/com/android/gallery3d/app/MoviePlayer.java
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -476,6 +476,14 @@
             if (mVideoView.isPlaying()) pauseVideo();
         }
     }
+
+    public int getAudioSessionId() {
+        return mVideoView.getAudioSessionId();
+    }
+
+    public void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) {
+        mVideoView.setOnPreparedListener(listener);
+    }
 }
 
 class Bookmarker {
diff --git a/src/com/android/gallery3d/ui/Knob.java b/src/com/android/gallery3d/ui/Knob.java
new file mode 100644
index 0000000..179023e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Knob.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import java.lang.Math;
+
+import com.android.gallery3d.R;
+
+public class Knob extends FrameLayout {
+    private static final int STROKE_WIDTH = 6;
+    private static final float TEXT_SIZE = 0.20f;
+    private static final float TEXT_PADDING = 0.31f;
+    private static final float LABEL_PADDING = 0.05f;
+    private static final float LABEL_SIZE = 0.09f;
+    private static final float LABEL_WIDTH = 0.80f;
+    private static final float INDICATOR_RADIUS = 0.38f;
+
+    public interface OnKnobChangeListener {
+        void onValueChanged(Knob knob, int value, boolean fromUser);
+        boolean onSwitchChanged(Knob knob, boolean on);
+    }
+
+    private OnKnobChangeListener mOnKnobChangeListener = null;
+
+    private float mProgress = 0.0f;
+    private int mMax = 100;
+    private boolean mOn = false;
+    private boolean mEnabled = false;
+
+    private int mHighlightColor;
+    private int mLowlightColor;
+    private int mDisabledColor;
+
+    private final Paint mPaint;
+
+    private final TextView mLabelTV;
+    private final TextView mProgressTV;
+
+    private final ImageView mKnobOn;
+    private final ImageView mKnobOff;
+
+    private float mLastX;
+    private float mLastY;
+    private boolean mMoved;
+
+    private int mWidth = 0;
+    private int mIndicatorWidth = 0;
+
+    private RectF mRectF;
+
+    public Knob(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Knob, 0, 0);
+
+        String label;
+        int foreground;
+        try {
+            label = a.getString(R.styleable.Knob_label);
+            foreground = a.getResourceId(R.styleable.Knob_foreground, R.drawable.knob);
+        } finally {
+            a.recycle();
+        }
+
+        LayoutInflater li = (LayoutInflater)
+                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        li.inflate(R.layout.knob, this, true);
+
+        Resources res = getResources();
+        mHighlightColor = res.getColor(R.color.highlight);
+        mLowlightColor = res.getColor(R.color.lowlight);
+        mDisabledColor = res.getColor(R.color.disabled_knob);
+
+        ((ImageView) findViewById(R.id.knob_foreground)).setImageResource(foreground);
+
+        mLabelTV = (TextView) findViewById(R.id.knob_label);
+        mLabelTV.setText(label);
+        mProgressTV = (TextView) findViewById(R.id.knob_value);
+
+        mKnobOn = (ImageView) findViewById(R.id.knob_toggle_on);
+        mKnobOff = (ImageView) findViewById(R.id.knob_toggle_off);
+
+        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mPaint.setColor(mHighlightColor);
+        mPaint.setStrokeWidth(STROKE_WIDTH);
+        mPaint.setStrokeCap(Paint.Cap.ROUND);
+        mPaint.setStyle(Paint.Style.STROKE);
+
+        setWillNotDraw(false);
+    }
+
+    public Knob(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public Knob(Context context) {
+        this(context, null);
+    }
+
+    public void setOnKnobChangeListener(OnKnobChangeListener l) {
+        mOnKnobChangeListener = l;
+    }
+
+    public void setValue(int value) {
+        if (mMax != 0) {
+            setProgress(((float) value) / mMax);
+        }
+    }
+
+    public int getValue() {
+        return (int) (mProgress * mMax);
+    }
+
+    public void setProgress(float progress) {
+        setProgress(progress, false);
+    }
+
+    private void setProgressText(boolean on) {
+        if (on) {
+            mProgressTV.setText((int) (mProgress * 100) + "%");
+        } else {
+            mProgressTV.setText("--%");
+        }
+    }
+
+    private void setProgress(float progress, boolean fromUser) {
+        if (progress > 1.0f) {
+            progress = 1.0f;
+        }
+        if (progress < 0.0f) {
+            progress = 0.0f;
+        }
+        mProgress = progress;
+        setProgressText(mOn && mEnabled);
+
+        invalidate();
+
+        if (mOnKnobChangeListener != null) {
+            mOnKnobChangeListener.onValueChanged(this, (int) (progress * mMax), fromUser);
+        }
+    }
+
+    public void setMax(int max) {
+        mMax = max;
+    }
+
+    public float getProgress() {
+        return mProgress;
+    }
+
+    private void drawIndicator() {
+        float r = mWidth * INDICATOR_RADIUS;
+        ImageView view = mOn ? mKnobOn : mKnobOff;
+        view.setTranslationX((float) Math.sin(mProgress * 2 * Math.PI) * r - mIndicatorWidth / 2);
+        view.setTranslationY((float) -Math.cos(mProgress * 2 * Math.PI) * r - mIndicatorWidth / 2);
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+        setOn(enabled);
+    }
+
+    public void setOn(boolean on) {
+        if (on != mOn) {
+            mOn = on;
+        }
+        on = on && mEnabled;
+        mLabelTV.setTextColor(on ? mHighlightColor : mDisabledColor);
+        mProgressTV.setTextColor(on ? mHighlightColor : mDisabledColor);
+        setProgressText(on);
+        mPaint.setColor(on ? mHighlightColor : mDisabledColor);
+        mKnobOn.setVisibility(on ? View.VISIBLE : View.GONE);
+        mKnobOff.setVisibility(on ? View.GONE : View.VISIBLE);
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        drawIndicator();
+        if (mOn && mEnabled) {
+            canvas.drawArc(mRectF, -90, mProgress * 360, false, mPaint);
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldW, int oldH) {
+        int size = w > h ? h : w;
+        mWidth = size;
+        mIndicatorWidth = mKnobOn.getWidth();
+
+        int diff;
+        if (w > h) {
+            diff = (w - h) / 2;
+            mRectF = new RectF(STROKE_WIDTH + diff, STROKE_WIDTH,
+                    w - STROKE_WIDTH - diff, h - STROKE_WIDTH);
+        } else {
+            diff = (h - w) / 2;
+            mRectF = new RectF(STROKE_WIDTH, STROKE_WIDTH + diff,
+                    w - STROKE_WIDTH, h - STROKE_WIDTH - diff);
+        }
+
+        mProgressTV.setTextSize(TypedValue.COMPLEX_UNIT_PX, size * TEXT_SIZE);
+        mProgressTV.setPadding(0, (int) (size * TEXT_PADDING), 0, 0);
+        mProgressTV.setVisibility(View.VISIBLE);
+        mLabelTV.setTextSize(TypedValue.COMPLEX_UNIT_PX, size * LABEL_SIZE);
+        mLabelTV.setPadding(0, (int) (size * LABEL_PADDING), 0, 0);
+        mLabelTV.setLayoutParams(new LinearLayout.LayoutParams((int) (w * LABEL_WIDTH),
+                    LayoutParams.WRAP_CONTENT));
+        mLabelTV.setVisibility(View.VISIBLE);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                if (mOn) {
+                    mLastX = event.getX();
+                    mLastY = event.getY();
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (mOn) {
+                    float x = event.getX();
+                    float y = event.getY();
+                    float center = mWidth / 2;
+                    if (mMoved || (x - center) * (x - center) + (y - center) * (y - center)
+                            > center * center / 4) {
+                        float delta = getDelta(x, y);
+                        setProgress(mProgress + delta / 360, true);
+                        mMoved = true;
+                    }
+                    mLastX = x;
+                    mLastY = y;
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (!mMoved) {
+                    if (mOnKnobChangeListener == null
+                            || mOnKnobChangeListener.onSwitchChanged(this, !mOn)) {
+                        if (mEnabled) {
+                            setOn(!mOn);
+                            invalidate();
+                        }
+                    }
+                }
+                mMoved = false;
+                break;
+            default:
+                break;
+        }
+        return true;
+    }
+
+    private float getDelta(float x, float y) {
+        float angle = angle(x, y);
+        float oldAngle = angle(mLastX, mLastY);
+        float delta = angle - oldAngle;
+        if (delta >= 180.0f) {
+            delta = -oldAngle;
+        } else if (delta <= -180.0f) {
+            delta = 360 - oldAngle;
+        }
+        return delta;
+    }
+
+    private float angle(float x, float y) {
+        float center = mWidth / 2.0f;
+        x -= center;
+        y -= center;
+
+        if (x == 0.0f) {
+            if (y > 0.0f) {
+                return 180.0f;
+            } else {
+                return 0.0f;
+            }
+        }
+
+        float angle = (float) (Math.atan(y / x) / Math.PI * 180.0);
+        if (x > 0.0f) {
+            angle += 90;
+        } else {
+            angle += 270;
+        }
+        return angle;
+    }
+}