summaryrefslogtreecommitdiffstats
path: root/src/org
diff options
context:
space:
mode:
authorSteve Kondik <shade@chemlab.org>2014-04-08 10:52:21 -0700
committerSteve Kondik <shade@chemlab.org>2014-04-08 10:52:21 -0700
commit3396e4fd07c7741bce6ff719295f8adf2c9e1869 (patch)
tree67f8d83ae6bafaf01f17a337473c9ad2e19f1fb1 /src/org
parent6ae11e2ccdb97317e29f2ebfa373e3fc744d7fcc (diff)
downloadandroid_packages_apps_AudioFX-3396e4fd07c7741bce6ff719295f8adf2c9e1869.tar.gz
android_packages_apps_AudioFX-3396e4fd07c7741bce6ff719295f8adf2c9e1869.tar.bz2
android_packages_apps_AudioFX-3396e4fd07c7741bce6ff719295f8adf2c9e1869.zip
Big refactor
* Replace custom widgets with stock ones * Remove dead code * Rename * Etc * Lots of stuff to do still
Diffstat (limited to 'src/org')
-rw-r--r--src/org/cyanogenmod/audiofx/ActivityMusic.java707
-rw-r--r--src/org/cyanogenmod/audiofx/Compatibility.java244
-rw-r--r--src/org/cyanogenmod/audiofx/ControlPanelEffect.java1490
-rw-r--r--src/org/cyanogenmod/audiofx/ControlPanelPicker.java121
-rw-r--r--src/org/cyanogenmod/audiofx/ControlPanelReceiver.java105
-rw-r--r--src/org/cyanogenmod/audiofx/OpenSLESConstants.java124
-rw-r--r--src/org/cyanogenmod/audiofx/seekbar/AbsSeekBar.java561
-rw-r--r--src/org/cyanogenmod/audiofx/seekbar/ProgressBar.java1146
-rw-r--r--src/org/cyanogenmod/audiofx/seekbar/SeekBar.java121
-rw-r--r--src/org/cyanogenmod/audiofx/widget/Biquad.java31
-rw-r--r--src/org/cyanogenmod/audiofx/widget/Complex.java94
-rw-r--r--src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java401
-rw-r--r--src/org/cyanogenmod/audiofx/widget/Gallery.java120
-rw-r--r--src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java59
-rw-r--r--src/org/cyanogenmod/audiofx/widget/Knob.java333
15 files changed, 5657 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/audiofx/ActivityMusic.java b/src/org/cyanogenmod/audiofx/ActivityMusic.java
new file mode 100644
index 0000000..d2985cc
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/ActivityMusic.java
@@ -0,0 +1,707 @@
+/*
+ * Copyright (C) 2010-2011 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 org.cyanogenmod.audiofx;
+
+import org.cyanogenmod.audiofx.widget.EqualizerSurface;
+import org.cyanogenmod.audiofx.widget.Gallery;
+import org.cyanogenmod.audiofx.widget.InterceptableLinearLayout;
+import org.cyanogenmod.audiofx.widget.Knob;
+import org.cyanogenmod.audiofx.widget.Knob.OnKnobChangeListener;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.audiofx.AudioEffect;
+import android.media.audiofx.AudioEffect.Descriptor;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ToggleButton;
+import android.util.DisplayMetrics;
+
+import java.util.Formatter;
+import java.util.Locale;
+import java.util.UUID;
+
+/**
+ *
+ */
+public class ActivityMusic extends Activity {
+ private final static String TAG = "AudioFXActivityMusic";
+
+ /**
+ * Max number of EQ bands supported
+ */
+ private final static int EQUALIZER_MAX_BANDS = 32;
+
+ /**
+ * Indicates if Virtualizer effect is supported.
+ */
+ private boolean mVirtualizerSupported;
+ private boolean mVirtualizerIsHeadphoneOnly;
+ /**
+ * Indicates if BassBoost effect is supported.
+ */
+ private boolean mBassBoostSupported;
+ /**
+ * Indicates if Equalizer effect is supported.
+ */
+ private boolean mEqualizerSupported;
+ /**
+ * Indicates if Preset Reverb effect is supported.
+ */
+ private boolean mPresetReverbSupported;
+
+ // Equalizer fields
+ private int mNumberEqualizerBands;
+ private int mEQPresetUserPos = 1;
+ private int mEQPreset;
+ private int[] mEQPresetUserBandLevelsPrev;
+ private String[] mEQPresetNames;
+ private String[] mReverbPresetNames;
+
+ private int mPRPreset;
+ private int mPRPresetPrevious;
+
+ private boolean mIsHeadsetOn = false;
+ private Switch mToggleSwitch;
+
+ private StringBuilder mFormatBuilder = new StringBuilder();
+ private Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+
+ // Preset Reverb fields
+ /**
+ * Array containing RSid of preset reverb names.
+ */
+ private static final int[] mReverbPresetRSids = {
+ R.string.none, R.string.smallroom, R.string.mediumroom, R.string.largeroom,
+ R.string.mediumhall, R.string.largehall, R.string.plate
+ };
+
+ /**
+ * Context field
+ */
+ private Context mContext;
+
+ /**
+ * Calling package name field
+ */
+ private String mCallingPackageName = "empty";
+
+ /**
+ * Audio session field
+ */
+ private int mAudioSession = AudioEffect.ERROR_BAD_VALUE;
+
+ // Broadcast receiver to handle wired and Bluetooth A2dp headset events
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ final boolean isHeadsetOnPrev = mIsHeadsetOn;
+ 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)) {
+ 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 = true;
+ }
+ } else if (action.equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
+ mIsHeadsetOn = audioManager.isBluetoothA2dpOn() || audioManager.isWiredHeadsetOn();
+ } else if (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 = audioManager.isWiredHeadsetOn();
+ }
+ }
+ if (isHeadsetOnPrev != mIsHeadsetOn) {
+ updateUIHeadset(true);
+ }
+ }
+ };
+
+ /*
+ * Declares and initializes all objects and widgets in the layouts
+ *
+ * (non-Javadoc)
+ *
+ * @see android.app.ActivityGroup#onCreate(android.os.Bundle)
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Init context to be used in listeners
+ mContext = this;
+
+ // Receive intent
+ // get calling intent
+ final Intent intent = getIntent();
+ mAudioSession = intent.getIntExtra(AudioEffect.EXTRA_AUDIO_SESSION,
+ AudioEffect.ERROR_BAD_VALUE);
+ Log.v(TAG, "audio session: " + mAudioSession);
+
+ mCallingPackageName = getCallingPackage();
+
+ // check for errors
+ if (mCallingPackageName == null) {
+ Log.e(TAG, "Package name is null");
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+ setResult(RESULT_OK);
+
+ Log.v(TAG, mCallingPackageName + " (" + mAudioSession + ")");
+
+ ControlPanelEffect.initEffectsPreferences(mContext, mCallingPackageName, mAudioSession);
+
+ // query available effects
+ final Descriptor[] effects = AudioEffect.queryEffects();
+
+ // Determine available/supported effects
+ Log.v(TAG, "Available effects:");
+ for (final Descriptor effect : effects) {
+ Log.v(TAG, effect.name.toString() + ", type: " + effect.type.toString());
+
+ if (effect.type.equals(AudioEffect.EFFECT_TYPE_VIRTUALIZER)) {
+ mVirtualizerSupported = true;
+ if (effect.uuid.equals(UUID.fromString("1d4033c0-8557-11df-9f2d-0002a5d5c51b"))
+ || effect.uuid.equals(UUID.fromString("e6c98a16-22a3-11e2-b87b-f23c91aec05e"))
+ || effect.uuid.equals(UUID.fromString("d3467faa-acc7-4d34-acaf-0002a5d5c51b"))) {
+ mVirtualizerIsHeadphoneOnly = true;
+ }
+ } else if (effect.type.equals(AudioEffect.EFFECT_TYPE_BASS_BOOST)) {
+ mBassBoostSupported = true;
+ } else if (effect.type.equals(AudioEffect.EFFECT_TYPE_EQUALIZER)) {
+ mEqualizerSupported = true;
+ } else if (effect.type.equals(AudioEffect.EFFECT_TYPE_PRESET_REVERB)) {
+ mPresetReverbSupported = true;
+ }
+ }
+
+ getWindow().requestFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ setContentView(R.layout.music_main);
+ final ViewGroup viewGroup = (ViewGroup) findViewById(R.id.contentSoundEffects);
+
+ // Fill array with presets from AudioEffects call.
+ // allocate a space for 2 extra strings (CI Extreme & User)
+ final int numPresets = ControlPanelEffect.getParameterInt(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.eq_num_presets);
+ mEQPresetNames = new String[numPresets + 2];
+ for (short i = 0; i < numPresets; i++) {
+ final String eqPresetName = ControlPanelEffect.getParameterString(mContext,
+ mCallingPackageName, mAudioSession, ControlPanelEffect.Key.eq_preset_name, i);
+ mEQPresetNames[i] = localizePresetName(eqPresetName);
+ }
+ mEQPresetNames[numPresets] = getString(R.string.ci_extreme);
+ mEQPresetNames[numPresets + 1] = getString(R.string.user);
+ mEQPresetUserPos = numPresets + 1;
+
+ // Load string resource of reverb presets
+ mReverbPresetNames = new String[mReverbPresetRSids.length];
+ for (short i = 0; i < mReverbPresetRSids.length; ++i) {
+ mReverbPresetNames[i] = getString(mReverbPresetRSids[i]);
+ }
+
+ // Watch for button clicks and initialization.
+ if ((mVirtualizerSupported) || (mBassBoostSupported) || (mEqualizerSupported)
+ || (mPresetReverbSupported)) {
+ // Set the listener for the main enhancements toggle button.
+ // Depending on the state enable the supported effects if they were
+ // checked in the setup tab.
+ mToggleSwitch = new Switch(this);
+ final int padding = getResources().getDimensionPixelSize(
+ R.dimen.action_bar_switch_padding);
+ mToggleSwitch.setPaddingRelative(0, 0, padding, 0);
+ mToggleSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(final CompoundButton buttonView,
+ final boolean isChecked) {
+ // set parameter and state
+ ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.global_enabled, isChecked);
+ // Enable Linear layout (in scroll layout) view with all
+ // effect contents depending on checked state
+ setEnabledAllChildren(viewGroup, isChecked);
+ // update UI according to headset state
+ updateUIHeadset(false);
+ setInterception(isChecked);
+ }
+ });
+
+ // Initialize the Virtualizer elements.
+ if (mVirtualizerSupported) {
+ final Knob knob = (Knob) findViewById(R.id.vIStrengthKnob);
+ knob.setMax(OpenSLESConstants.VIRTUALIZER_MAX_STRENGTH -
+ OpenSLESConstants.VIRTUALIZER_MIN_STRENGTH);
+ knob.setOnKnobChangeListener(new OnKnobChangeListener() {
+ // Update the parameters while Knob changes and set the
+ // effect parameter.
+ @Override
+ public void onValueChanged(final Knob knob, final int value,
+ final boolean fromUser) {
+ // set parameter and state
+ ControlPanelEffect.setParameterInt(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.virt_strength, value);
+ }
+
+ @Override
+ public boolean onSwitchChanged(final Knob knob, boolean on) {
+ if (on && !mIsHeadsetOn) {
+ showHeadsetMsg();
+ return false;
+ }
+ ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.virt_enabled, on);
+ return true;
+ }
+ });
+ }
+
+ // Initialize the Bass Boost elements.
+ if (mBassBoostSupported) {
+ final Knob knob = (Knob) findViewById(R.id.bBStrengthKnob);
+ knob.setMax(OpenSLESConstants.BASSBOOST_MAX_STRENGTH
+ - OpenSLESConstants.BASSBOOST_MIN_STRENGTH);
+ knob.setOnKnobChangeListener(new OnKnobChangeListener() {
+ // Update the parameters while SeekBar changes and set the
+ // effect parameter.
+
+ @Override
+ public void onValueChanged(final Knob knob, final int value,
+ final boolean fromUser) {
+ // set parameter and state
+ ControlPanelEffect.setParameterInt(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.bb_strength, value);
+ }
+
+ @Override
+ public boolean onSwitchChanged(final Knob knob,boolean on) {
+ if (on && !mIsHeadsetOn) {
+ showHeadsetMsg();
+ return false;
+ }
+ ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.bb_enabled, on);
+ return true;
+ }
+ });
+ }
+
+ // Initialize the Equalizer elements.
+ if (mEqualizerSupported) {
+ mEQPreset = ControlPanelEffect.getParameterInt(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.eq_current_preset);
+ if (mEQPreset >= mEQPresetNames.length) {
+ mEQPreset = 0;
+ }
+ equalizerPresetsInit((Gallery)findViewById(R.id.eqPresets));
+ equalizerBandsInit();
+ }
+
+ // Initialize the Preset Reverb elements.
+ // Set Spinner listeners.
+ if (mPresetReverbSupported) {
+ mPRPreset = ControlPanelEffect.getParameterInt(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.pr_current_preset);
+ mPRPresetPrevious = mPRPreset;
+ reverbSpinnerInit((Spinner)findViewById(R.id.prSpinner));
+ }
+
+ } else {
+ viewGroup.setVisibility(View.GONE);
+ ((TextView) findViewById(R.id.noEffectsTextView)).setVisibility(View.VISIBLE);
+ }
+
+ ActionBar ab = getActionBar();
+ final ActionBar.LayoutParams params = new ActionBar.LayoutParams(
+ ActionBar.LayoutParams.WRAP_CONTENT,
+ ActionBar.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER_VERTICAL | Gravity.END);
+ ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM
+ | ActionBar.DISPLAY_HOME_AS_UP);
+ ab.setCustomView(mToggleSwitch, params);
+ }
+
+ private final String localizePresetName(final String name) {
+ final String[] names = {
+ "Normal", "Classical", "Dance", "Flat", "Folk",
+ "Heavy Metal", "Hip Hop", "Jazz", "Pop", "Rock"
+ };
+ final int[] ids = {
+ R.string.normal, R.string.classical, R.string.dance, R.string.flat, R.string.folk,
+ R.string.heavy_metal, R.string.hip_hop, R.string.jazz, R.string.pop, R.string.rock
+ };
+
+ for (int i = names.length - 1; i >= 0; --i) {
+ if (names[i].equals(name)) {
+ return getString(ids[i]);
+ }
+ }
+ return name;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.app.Activity#onResume()
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if ((mVirtualizerSupported) || (mBassBoostSupported) || (mEqualizerSupported)
+ || (mPresetReverbSupported)) {
+ // Listen for broadcast intents that might affect the onscreen UI for headset.
+ 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);
+
+ // Check if wired or Bluetooth headset is connected/on
+ final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ mIsHeadsetOn = (audioManager.isWiredHeadsetOn() || audioManager.isBluetoothA2dpOn());
+ Log.v(TAG, "onResume: mIsHeadsetOn : " + mIsHeadsetOn);
+
+ // Update UI
+ updateUI();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.app.Activity#onPause()
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ // Unregister for broadcast intents. (These affect the visible UI,
+ // so we only care about them while we're in the foreground.)
+ unregisterReceiver(mReceiver);
+ }
+
+ private void reverbSpinnerInit(Spinner spinner) {
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, mReverbPresetNames);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+ spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (position != mPRPresetPrevious) {
+ presetReverbSetPreset(position);
+ }
+ mPRPresetPrevious = position;
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+ spinner.setSelection(mPRPreset);
+ }
+
+ private void equalizerPresetsInit(Gallery gallery) {
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.equalizer_presets,
+ mEQPresetNames);
+
+ gallery.setAdapter(adapter);
+ gallery.setOnItemSelectedListener(new Gallery.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(int position) {
+ mEQPreset = position;
+ equalizerSetPreset(position);
+ }
+ });
+ gallery.setSelection(mEQPreset);
+ }
+
+
+ /**
+ * En/disables all children for a given view. For linear and relative layout children do this
+ * recursively
+ *
+ * @param viewGroup
+ * @param enabled
+ */
+ private void setEnabledAllChildren(final ViewGroup viewGroup, final boolean enabled) {
+ final int count = viewGroup.getChildCount();
+ final View bb = findViewById(R.id.bBStrengthKnob);
+ final View virt = findViewById(R.id.vIStrengthKnob);
+ final View eq = findViewById(R.id.frequencyResponse);
+ boolean on = true;
+
+ for (int i = 0; i < count; i++) {
+ final View view = viewGroup.getChildAt(i);
+ if ((view instanceof LinearLayout) || (view instanceof RelativeLayout)) {
+ final ViewGroup vg = (ViewGroup) view;
+ setEnabledAllChildren(vg, enabled);
+ }
+
+ if (enabled && view == virt) {
+ on = ControlPanelEffect.getParameterBoolean(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.virt_enabled);
+ view.setEnabled(on);
+ } else if (enabled && view == bb) {
+ on = ControlPanelEffect.getParameterBoolean(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.bb_enabled);
+ view.setEnabled(on);
+ } else if (enabled && view == eq) {
+ view.setEnabled(true);
+ } else {
+ view.setEnabled(enabled);
+ }
+ }
+ }
+
+ /**
+ * Updates UI (checkbox, seekbars, enabled states) according to the current stored preferences.
+ */
+ private void updateUI() {
+ final boolean isEnabled = ControlPanelEffect.getParameterBoolean(mContext,
+ mCallingPackageName, mAudioSession, ControlPanelEffect.Key.global_enabled);
+ mToggleSwitch.setChecked(isEnabled);
+ setEnabledAllChildren((ViewGroup) findViewById(R.id.contentSoundEffects), isEnabled);
+ updateUIHeadset(false);
+
+ if (mVirtualizerSupported) {
+ Knob knob = (Knob) findViewById(R.id.vIStrengthKnob);
+ int strength = ControlPanelEffect
+ .getParameterInt(mContext, mCallingPackageName, mAudioSession,
+ ControlPanelEffect.Key.virt_strength);
+ knob.setValue(strength);
+ boolean hasStrength = ControlPanelEffect.getParameterBoolean(mContext,
+ mCallingPackageName, mAudioSession,
+ ControlPanelEffect.Key.virt_strength_supported);
+ if (!hasStrength) {
+ knob.setVisibility(View.GONE);
+ }
+ }
+ if (mBassBoostSupported) {
+ ((Knob) findViewById(R.id.bBStrengthKnob)).setValue(ControlPanelEffect
+ .getParameterInt(mContext, mCallingPackageName, mAudioSession,
+ ControlPanelEffect.Key.bb_strength));
+ }
+ if (mEqualizerSupported) {
+ equalizerUpdateDisplay();
+ }
+ if (mPresetReverbSupported) {
+ int reverb = ControlPanelEffect.getParameterInt(
+ mContext, mCallingPackageName, mAudioSession,
+ ControlPanelEffect.Key.pr_current_preset);
+ ((Spinner)findViewById(R.id.prSpinner)).setSelection(reverb);
+ }
+
+ setInterception(isEnabled);
+ }
+
+ private void setInterception(boolean isEnabled) {
+ final InterceptableLinearLayout ill =
+ (InterceptableLinearLayout) findViewById(R.id.contentSoundEffects);
+ ill.setInterception(!isEnabled);
+ if (isEnabled) {
+ ill.setOnClickListener(null);
+ } else {
+ ill.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Toast toast = Toast.makeText(mContext,
+ getString(R.string.power_on_prompt), Toast.LENGTH_SHORT);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ }
+ });
+ }
+ }
+
+ /**
+ * Updates UI for headset mode. En/disable VI and BB controls depending on headset state
+ * (on/off) if effects are on. Do the inverse for their layouts so they can take over
+ * control/events.
+ */
+ private void updateUIHeadset(boolean force) {
+ boolean enabled = mToggleSwitch.isChecked() && mIsHeadsetOn;
+ final Knob bBKnob = (Knob) findViewById(R.id.bBStrengthKnob);
+ bBKnob.setEnabled(enabled);
+ final Knob vIKnob = (Knob) findViewById(R.id.vIStrengthKnob);
+ vIKnob.setEnabled(enabled || !mVirtualizerIsHeadphoneOnly);
+
+ if (!force) {
+ boolean on = ControlPanelEffect.getParameterBoolean(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.bb_enabled);
+ bBKnob.setOn(enabled && on);
+ on = ControlPanelEffect.getParameterBoolean(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.virt_enabled);
+ vIKnob.setOn((enabled && on) || !mVirtualizerIsHeadphoneOnly);
+ }
+ }
+
+ /**
+ * Initializes the equalizer elements. Set the SeekBars and Spinner listeners.
+ */
+ private void equalizerBandsInit() {
+ // Initialize the N-Band Equalizer elements.
+ mNumberEqualizerBands = ControlPanelEffect.getParameterInt(mContext, mCallingPackageName,
+ mAudioSession, ControlPanelEffect.Key.eq_num_bands);
+ mEQPresetUserBandLevelsPrev = ControlPanelEffect.getParameterIntArray(mContext,
+ mCallingPackageName, mAudioSession,
+ ControlPanelEffect.Key.eq_preset_user_band_level);
+ final int[] centerFreqs = ControlPanelEffect.getParameterIntArray(mContext,
+ mCallingPackageName, mAudioSession, ControlPanelEffect.Key.eq_center_freq);
+ final int[] bandLevelRange = ControlPanelEffect.getParameterIntArray(mContext,
+ mCallingPackageName, mAudioSession, ControlPanelEffect.Key.eq_level_range);
+ final EqualizerSurface eq = (EqualizerSurface)findViewById(R.id.frequencyResponse);
+ float[] centerFreqsKHz = new float[centerFreqs.length];
+ for (int i = 0; i < centerFreqs.length; i++) {
+ centerFreqsKHz[i] = (float)centerFreqs[i] / 1000.0f;
+ }
+ eq.setCenterFreqs(centerFreqsKHz);
+ eq.setBandLevelRange(bandLevelRange[0] / 100, bandLevelRange[1] / 100);
+
+ final EqualizerSurface.BandUpdatedListener listener = new EqualizerSurface.BandUpdatedListener() {
+ @Override
+ public void onBandUpdated(int band, float dB) {
+ if (mEQPreset != mEQPresetUserPos) {
+
+ }
+ equalizerBandUpdate(band, (int)(dB * 100));
+
+ }
+ };
+ eq.registerBandUpdatedListener(listener);
+ }
+
+ private String format(String format, Object... args) {
+ mFormatBuilder.setLength(0);
+ mFormatter.format(format, args);
+ return mFormatBuilder.toString();
+ }
+
+ /**
+ * Updates the EQ by getting the parameters.
+ */
+ private void equalizerUpdateDisplay() {
+ // Update and show the active N-Band Equalizer bands.
+ final int[] bandLevels = ControlPanelEffect.getParameterIntArray(mContext,
+ mCallingPackageName, mAudioSession, ControlPanelEffect.Key.eq_band_level);
+ EqualizerSurface eq = (EqualizerSurface)findViewById(R.id.frequencyResponse);
+ for (short band = 0; band < mNumberEqualizerBands; band++) {
+ final int level = bandLevels[band];
+ eq.setBand(band, (float)level / 100.0f);
+ }
+ }
+
+ /**
+ * Updates/sets a given EQ band level.
+ *
+ * @param band
+ * Band id
+ * @param level
+ * EQ band level
+ */
+ private void equalizerBandUpdate(final int band, final int level) {
+ ControlPanelEffect.setParameterInt(mContext, mCallingPackageName, mAudioSession,
+ ControlPanelEffect.Key.eq_band_level, level, band);
+ }
+
+ /**
+ * Sets the given EQ preset.
+ *
+ * @param preset
+ * EQ preset id.
+ */
+ private void equalizerSetPreset(final int preset) {
+ ControlPanelEffect.setParameterInt(mContext, mCallingPackageName, mAudioSession,
+ ControlPanelEffect.Key.eq_current_preset, preset);
+ equalizerUpdateDisplay();
+ }
+
+ /**
+ * Sets the given PR preset.
+ *
+ * @param preset
+ * PR preset id.
+ */
+ private void presetReverbSetPreset(final int preset) {
+ ControlPanelEffect.setParameterInt(mContext, mCallingPackageName, mAudioSession,
+ ControlPanelEffect.Key.pr_current_preset, preset);
+ }
+
+ /**
+ * Show msg that headset needs to be plugged.
+ */
+ private void showHeadsetMsg() {
+ final Context context = getApplicationContext();
+ final int duration = Toast.LENGTH_SHORT;
+
+ final Toast toast = Toast.makeText(context, getString(R.string.headset_plug), duration);
+ toast.setGravity(Gravity.CENTER, toast.getXOffset() / 2, toast.getYOffset() / 2);
+ toast.show();
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/Compatibility.java b/src/org/cyanogenmod/audiofx/Compatibility.java
new file mode 100644
index 0000000..c0e72c2
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/Compatibility.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2011 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 org.cyanogenmod.audiofx;
+
+import android.app.Activity;
+import android.app.IntentService;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.audiofx.AudioEffect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Provide backwards compatibility for existing control panels.
+ * There are two major parts to this:
+ * - a BroadcastReceiver that listens for installed or removed packages, and
+ * enables or disables control panel receivers as needed to ensure that only
+ * one control panel package will receive the broadcasts that applications end
+ * - a high priority control panel activity that redirects to the currently
+ * selected control panel activity
+ *
+ */
+public class Compatibility {
+
+ private final static String TAG = "AudioFXCompat";
+ // run "setprop log.tag.AudioFXCompat DEBUG" to turn on logging
+ private final static boolean LOG = Log.isLoggable(TAG, Log.DEBUG);
+
+
+ /**
+ * This activity has an intent filter with the highest possible priority, so
+ * it will always be chosen. It then looks up the correct control panel to
+ * use and launches that.
+ */
+ public static class Redirector extends Activity {
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ log("Compatibility Activity called from " + getCallingPackage());
+ Intent i = new Intent(getIntent());
+ i.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ SharedPreferences pref = getSharedPreferences("musicfx", MODE_PRIVATE);
+ String defPackage = pref.getString("defaultpanelpackage", null);
+ String defName = pref.getString("defaultpanelname", null);
+ log("read " + defPackage + "/" + defName + " as default");
+ if (defPackage == null || defName == null) {
+ Log.e(TAG, "no default set!");
+ // use the built-in panel
+ i.setComponent(new ComponentName(this, ActivityMusic.class));
+ // also save it as the default
+ Intent updateIntent = new Intent(this, Service.class);
+ updateIntent.putExtra("defPackage", getPackageName());
+ updateIntent.putExtra("defName", ActivityMusic.class.getName());
+ startService(updateIntent);
+ } else {
+ i.setComponent(new ComponentName(defPackage, defName));
+ }
+ startActivity(i);
+ finish();
+ }
+ }
+
+ /**
+ * This BroadcastReceiver responds to BOOT_COMPLETED, PACKAGE_ADDED,
+ * PACKAGE_REPLACED and PACKAGE_REMOVED intents. When run, it checks
+ * to see whether the active control panel needs to be updated:
+ * - if there is no default, it picks one
+ * - if a new control panel is installed, it becomes the default
+ * It then enables the open/close receivers in the active control panel,
+ * and disables them in the others.
+ */
+ public static class Receiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+
+ log("received");
+ Intent updateIntent = new Intent(context, Service.class);
+ updateIntent.putExtra("reason", intent);
+ context.startService(updateIntent);
+ }
+ }
+
+ public static class Service extends IntentService {
+
+ PackageManager mPackageManager;
+
+ public Service() {
+ super("CompatibilityService");
+ }
+
+ @Override
+ protected void onHandleIntent(final Intent intent) {
+ log("handleintent");
+ if (mPackageManager == null) {
+ mPackageManager = getPackageManager();
+ }
+
+ String defPackage = intent.getStringExtra("defPackage");
+ String defName = intent.getStringExtra("defName");
+ if (defPackage != null && defName != null) {
+ setDefault(defPackage, defName);
+ return;
+ }
+
+ Intent packageIntent = intent.getParcelableExtra("reason");
+ Bundle b = packageIntent.getExtras();
+ if (b != null) b.size();
+ log("intentservice saw: " + packageIntent + " " + b);
+ // TODO, be smarter about package upgrades (which results in three
+ // broadcasts: removed, added, replaced)
+ Uri packageUri = packageIntent.getData();
+ String updatedPackage = null;
+ if (packageUri != null) {
+ updatedPackage = packageUri.toString().substring(8);
+ pickDefaultControlPanel(updatedPackage);
+ }
+ }
+
+ private void pickDefaultControlPanel(String updatedPackage) {
+
+ ResolveInfo defPanel = null;
+ ResolveInfo otherPanel = null;
+ ResolveInfo thisPanel = null;
+ Intent i = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
+ List<ResolveInfo> ris = mPackageManager.queryIntentActivities(i, PackageManager.GET_DISABLED_COMPONENTS);
+ log("found: " + ris.size());
+ SharedPreferences pref = getSharedPreferences("musicfx", MODE_PRIVATE);
+ String savedDefPackage = pref.getString("defaultpanelpackage", null);
+ String savedDefName = pref.getString("defaultpanelname", null);
+ log("saved default: " + savedDefName);
+ for (ResolveInfo foo: ris) {
+ if (foo.activityInfo.name.equals(Compatibility.Redirector.class.getName())) {
+ log("skipping " + foo);
+ continue;
+ }
+ log("considering " + foo);
+ if (foo.activityInfo.name.equals(savedDefName) &&
+ foo.activityInfo.packageName.equals(savedDefPackage) &&
+ foo.activityInfo.enabled) {
+ log("default: " + savedDefName);
+ defPanel = foo;
+ break;
+ } else if (foo.activityInfo.packageName.equals(updatedPackage)) {
+ log("choosing newly installed package " + updatedPackage);
+ otherPanel = foo;
+ } else if (otherPanel == null && !foo.activityInfo.packageName.equals(getPackageName())) {
+ otherPanel = foo;
+ } else {
+ thisPanel = foo;
+ }
+ }
+
+ if (defPanel == null) {
+ // pick a default control panel
+ if (otherPanel == null) {
+ if (thisPanel == null) {
+ Log.e(TAG, "No control panels found!");
+ return;
+ }
+ otherPanel = thisPanel;
+ }
+ defPanel = otherPanel;
+ }
+
+ // Now that we have selected a default control panel activity, ensure
+ // that the broadcast receiver(s) in that same package are enabled,
+ // and the ones in the other packages are disabled.
+ String defPackage = defPanel.activityInfo.packageName;
+ String defName = defPanel.activityInfo.name;
+ setDefault(defPackage, defName);
+ }
+
+ private void setDefault(String defPackage, String defName) {
+ Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
+ List<ResolveInfo> ris = mPackageManager.queryBroadcastReceivers(i, PackageManager.GET_DISABLED_COMPONENTS);
+ setupReceivers(ris, defPackage);
+ // The open and close receivers are likely the same, but they may not be.
+ i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
+ ris = mPackageManager.queryBroadcastReceivers(i, PackageManager.GET_DISABLED_COMPONENTS);
+ setupReceivers(ris, defPackage);
+
+ // Write the selected default to the prefs so that the Redirector activity
+ // knows which one to use.
+ SharedPreferences pref = getSharedPreferences("musicfx", MODE_PRIVATE);
+ Editor ed = pref.edit();
+ ed.putString("defaultpanelpackage", defPackage);
+ ed.putString("defaultpanelname", defName);
+ ed.commit();
+ log("wrote " + defPackage + "/" + defName + " as default");
+ }
+
+ private void setupReceivers(List<ResolveInfo> ris, String defPackage) {
+ // TODO - we may need to keep track of active sessions and send "open session"
+ // broadcast to newly enabled receivers, while sending "close session" to
+ // receivers that are about to be disabled. We could also consider just
+ // killing the process hosting the disabled components.
+ for (ResolveInfo foo: ris) {
+ ComponentName comp = new ComponentName(foo.activityInfo.packageName, foo.activityInfo.name);
+ if (foo.activityInfo.packageName.equals(defPackage)) {
+ log("enabling receiver " + foo);
+ mPackageManager.setComponentEnabledSetting(comp,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP);
+ } else {
+ log("disabling receiver " + foo);
+ mPackageManager.setComponentEnabledSetting(comp,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ }
+ }
+ }
+ }
+
+ private static void log(String out) {
+ if (LOG) {
+ Log.d(TAG, out);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/ControlPanelEffect.java b/src/org/cyanogenmod/audiofx/ControlPanelEffect.java
new file mode 100644
index 0000000..f028105
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/ControlPanelEffect.java
@@ -0,0 +1,1490 @@
+/*
+ * Copyright (C) 2010-2011 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 org.cyanogenmod.audiofx;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.media.MediaPlayer;
+import android.media.audiofx.AudioEffect;
+import android.media.audiofx.BassBoost;
+import android.media.audiofx.Equalizer;
+import android.media.audiofx.PresetReverb;
+import android.media.audiofx.Virtualizer;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * The Common class defines constants to be used by the control panels.
+ */
+public class ControlPanelEffect {
+
+ private final static String TAG = "AudioFXControlPanelEffect";
+
+ /**
+ * Audio session priority
+ */
+ private static final int PRIORITY = 0;
+
+ /**
+ * The control mode specifies if control panel updates effects and preferences or only
+ * preferences.
+ */
+ static enum ControlMode {
+ /**
+ * Control panel updates effects and preferences. Applicable when audio session is delivered
+ * by user.
+ */
+ CONTROL_EFFECTS,
+ /**
+ * Control panel only updates preferences. Applicable when there was no audio or invalid
+ * session provided by user.
+ */
+ CONTROL_PREFERENCES
+ }
+
+ static enum Key {
+ global_enabled, virt_enabled, virt_strength_supported, virt_strength, virt_type, bb_enabled,
+ bb_strength, te_enabled, te_strength, avl_enabled, lm_enabled, lm_strength, eq_enabled,
+ eq_num_bands, eq_level_range, eq_center_freq, eq_band_level, eq_band_level_no_save,
+ eq_num_presets, eq_preset_name, eq_preset_user_band_level,
+ eq_preset_user_band_level_default, eq_preset_opensl_es_band_level,
+ eq_preset_ci_extreme_band_level, eq_current_preset,
+ pr_enabled, pr_current_preset
+ }
+
+ // Effect/audio session Mappings
+ /**
+ * Hashmap initial capacity
+ */
+ private static final int HASHMAP_INITIAL_CAPACITY = 16;
+ /**
+ * Hashmap load factor
+ */
+ private static final float HASHMAP_LOAD_FACTOR = 0.75f;
+ /**
+ * ConcurrentHashMap concurrency level
+ */
+ private static final int HASHMAP_CONCURRENCY_LEVEL = 2;
+
+ /**
+ * Map containing the Virtualizer audio session, effect mappings.
+ */
+ private static final ConcurrentHashMap<Integer, Virtualizer> mVirtualizerInstances = new ConcurrentHashMap<Integer, Virtualizer>(
+ HASHMAP_INITIAL_CAPACITY, HASHMAP_LOAD_FACTOR, HASHMAP_CONCURRENCY_LEVEL);
+ /**
+ * Map containing the BB audio session, effect mappings.
+ */
+ private static final ConcurrentHashMap<Integer, BassBoost> mBassBoostInstances = new ConcurrentHashMap<Integer, BassBoost>(
+ HASHMAP_INITIAL_CAPACITY, HASHMAP_LOAD_FACTOR, HASHMAP_CONCURRENCY_LEVEL);
+ /**
+ * Map containing the EQ audio session, effect mappings.
+ */
+ private static final ConcurrentHashMap<Integer, Equalizer> mEQInstances = new ConcurrentHashMap<Integer, Equalizer>(
+ HASHMAP_INITIAL_CAPACITY, HASHMAP_LOAD_FACTOR, HASHMAP_CONCURRENCY_LEVEL);
+ /**
+ * Map containing the PR audio session, effect mappings.
+ */
+ private static final ConcurrentHashMap<Integer, PresetReverb> mPresetReverbInstances = new ConcurrentHashMap<Integer, PresetReverb>(
+ HASHMAP_INITIAL_CAPACITY, HASHMAP_LOAD_FACTOR, HASHMAP_CONCURRENCY_LEVEL);
+
+ /**
+ * Map containing the package name, audio session mappings.
+ */
+ private static final ConcurrentHashMap<String, Integer> mPackageSessions = new ConcurrentHashMap<String, Integer>(
+ HASHMAP_INITIAL_CAPACITY, HASHMAP_LOAD_FACTOR, HASHMAP_CONCURRENCY_LEVEL);
+
+ // Defaults
+ final static boolean GLOBAL_ENABLED_DEFAULT = false;
+ private final static boolean VIRTUALIZER_ENABLED_DEFAULT = true;
+ private final static int VIRTUALIZER_STRENGTH_DEFAULT = 1000;
+ private final static boolean BASS_BOOST_ENABLED_DEFAULT = true;
+ private final static int BASS_BOOST_STRENGTH_DEFAULT = 667;
+ private final static boolean PRESET_REVERB_ENABLED_DEFAULT = true;
+ private final static int PRESET_REVERB_CURRENT_PRESET_DEFAULT = 0; // None
+ private static int mPrevBassBoostStrength = 0;
+ private static int mPrevVirtStrength = 0;
+
+ // EQ defaults
+ private final static boolean EQUALIZER_ENABLED_DEFAULT = true;
+ private final static String EQUALIZER_PRESET_NAME_DEFAULT = "Preset";
+ private final static short EQUALIZER_NUMBER_BANDS_DEFAULT = 5;
+ private final static short EQUALIZER_NUMBER_PRESETS_DEFAULT = 0;
+ private final static short[] EQUALIZER_BAND_LEVEL_RANGE_DEFAULT = { -1500, 1500 };
+ private final static int[] EQUALIZER_CENTER_FREQ_DEFAULT = { 60000, 230000, 910000, 3600000,
+ 14000000 };
+ private final static short[] EQUALIZER_PRESET_CIEXTREME_BAND_LEVEL = { 0, 800, 400, 100, 1000 };
+ private final static short[] EQUALIZER_PRESET_USER_BAND_LEVEL_DEFAULT = { 0, 0, 0, 0, 0 };
+ private final static short[][] EQUALIZER_PRESET_OPENSL_ES_BAND_LEVEL_DEFAULT = new short[EQUALIZER_NUMBER_PRESETS_DEFAULT][EQUALIZER_NUMBER_BANDS_DEFAULT];
+
+ // EQ effect properties which are invariable over all EQ effects sessions
+ private static short[] mEQBandLevelRange = EQUALIZER_BAND_LEVEL_RANGE_DEFAULT;
+ private static short mEQNumBands = EQUALIZER_NUMBER_BANDS_DEFAULT;
+ private static int[] mEQCenterFreq = EQUALIZER_CENTER_FREQ_DEFAULT;
+ private static short mEQNumPresets = EQUALIZER_NUMBER_PRESETS_DEFAULT;
+ private static short[][] mEQPresetOpenSLESBandLevel = EQUALIZER_PRESET_OPENSL_ES_BAND_LEVEL_DEFAULT;
+ private static String[] mEQPresetNames;
+ private static boolean mIsEQInitialized = false;
+ private final static Object mEQInitLock = new Object();
+
+ /**
+ * Default int argument used in methods to see that the arg is a dummy. Used for method
+ * overloading.
+ */
+ private final static int DUMMY_ARGUMENT = -1;
+
+ /**
+ * Inits effects preferences for the given context and package name in the control panel. If
+ * preferences for the given package name don't exist, they are created and initialized.
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ */
+ public static void initEffectsPreferences(final Context context, final String packageName,
+ final int audioSession) {
+ final SharedPreferences prefs = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE);
+ final SharedPreferences.Editor editor = prefs.edit();
+ final ControlMode controlMode = getControlMode(audioSession);
+
+ // init preferences
+ try {
+ // init global on/off switch
+ final boolean isGlobalEnabled = prefs.getBoolean(Key.global_enabled.toString(),
+ GLOBAL_ENABLED_DEFAULT);
+ editor.putBoolean(Key.global_enabled.toString(), isGlobalEnabled);
+ Log.v(TAG, "isGlobalEnabled = " + isGlobalEnabled);
+
+ // Virtualizer
+ final boolean isVIEnabled = prefs.getBoolean(Key.virt_enabled.toString(),
+ VIRTUALIZER_ENABLED_DEFAULT);
+ final Virtualizer virt = new Virtualizer(0, audioSession);
+ final int vIStrength = prefs.getInt(Key.virt_strength.toString(),
+ virt.getRoundedStrength());
+ virt.release();
+ editor.putBoolean(Key.virt_enabled.toString(), isVIEnabled);
+ editor.putInt(Key.virt_strength.toString(), vIStrength);
+ {
+ final MediaPlayer mediaPlayer = new MediaPlayer();
+ final int session = mediaPlayer.getAudioSessionId();
+ Virtualizer virtualizerEffect = null;
+ try {
+ virtualizerEffect = new Virtualizer(PRIORITY, session);
+ editor.putBoolean(Key.virt_strength_supported.toString(),
+ virtualizerEffect.getStrengthSupported());
+ } finally {
+ if (virtualizerEffect != null) {
+ Log.d(TAG, "Releasing dummy Virtualizer effect");
+ virtualizerEffect.release();
+ }
+ mediaPlayer.release();
+ }
+ }
+
+ // BassBoost
+ final boolean isBBEnabled = prefs.getBoolean(Key.bb_enabled.toString(),
+ BASS_BOOST_ENABLED_DEFAULT);
+ final int bBStrength = prefs.getInt(Key.bb_strength.toString(),
+ BASS_BOOST_STRENGTH_DEFAULT);
+ editor.putBoolean(Key.bb_enabled.toString(), isBBEnabled);
+ editor.putInt(Key.bb_strength.toString(), bBStrength);
+
+ // Equalizer
+ synchronized (mEQInitLock) {
+ // If EQ is not initialized already create "dummy" audio session created by
+ // MediaPlayer and create effect on it to retrieve the invariable EQ properties
+ if (!mIsEQInitialized) {
+ final MediaPlayer mediaPlayer = new MediaPlayer();
+ final int session = mediaPlayer.getAudioSessionId();
+ Equalizer equalizerEffect = null;
+ try {
+ Log.d(TAG, "Creating dummy EQ effect on session " + session);
+ equalizerEffect = new Equalizer(PRIORITY, session);
+
+ mEQBandLevelRange = equalizerEffect.getBandLevelRange();
+ mEQNumBands = equalizerEffect.getNumberOfBands();
+ mEQCenterFreq = new int[mEQNumBands];
+ for (short band = 0; band < mEQNumBands; band++) {
+ mEQCenterFreq[band] = equalizerEffect.getCenterFreq(band);
+ }
+ mEQNumPresets = equalizerEffect.getNumberOfPresets();
+ mEQPresetNames = new String[mEQNumPresets];
+ mEQPresetOpenSLESBandLevel = new short[mEQNumPresets][mEQNumBands];
+ for (short preset = 0; preset < mEQNumPresets; preset++) {
+ mEQPresetNames[preset] = equalizerEffect.getPresetName(preset);
+ equalizerEffect.usePreset(preset);
+ for (short band = 0; band < mEQNumBands; band++) {
+ mEQPresetOpenSLESBandLevel[preset][band] = equalizerEffect
+ .getBandLevel(band);
+ }
+ }
+
+ mIsEQInitialized = true;
+ } catch (final IllegalStateException e) {
+ Log.e(TAG, "Equalizer: " + e);
+ } catch (final IllegalArgumentException e) {
+ Log.e(TAG, "Equalizer: " + e);
+ } catch (final UnsupportedOperationException e) {
+ Log.e(TAG, "Equalizer: " + e);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "Equalizer: " + e);
+ } finally {
+ if (equalizerEffect != null) {
+ Log.d(TAG, "Releasing dummy EQ effect");
+ equalizerEffect.release();
+ }
+ mediaPlayer.release();
+
+ // When there was a failure set some good defaults
+ if (!mIsEQInitialized) {
+ mEQPresetOpenSLESBandLevel = new short[mEQNumPresets][mEQNumBands];
+ for (short preset = 0; preset < mEQNumPresets; preset++) {
+ // Init preset names to a dummy name
+ mEQPresetNames[preset] = prefs.getString(
+ Key.eq_preset_name.toString() + preset,
+ EQUALIZER_PRESET_NAME_DEFAULT + preset);
+ if (preset < EQUALIZER_PRESET_OPENSL_ES_BAND_LEVEL_DEFAULT.length) {
+ mEQPresetOpenSLESBandLevel[preset] = Arrays.copyOf(
+ EQUALIZER_PRESET_OPENSL_ES_BAND_LEVEL_DEFAULT[preset],
+ mEQNumBands);
+ }
+ }
+ }
+ }
+ }
+ editor.putInt(Key.eq_level_range.toString() + 0, mEQBandLevelRange[0]);
+ editor.putInt(Key.eq_level_range.toString() + 1, mEQBandLevelRange[1]);
+ editor.putInt(Key.eq_num_bands.toString(), mEQNumBands);
+ editor.putInt(Key.eq_num_presets.toString(), mEQNumPresets);
+ // Resetting the EQ arrays depending on the real # bands with defaults if
+ // band < default size else 0 by copying default arrays over new ones
+ final short[] eQPresetCIExtremeBandLevel = Arrays.copyOf(
+ EQUALIZER_PRESET_CIEXTREME_BAND_LEVEL, mEQNumBands);
+ final short[] eQPresetUserBandLevelDefault = Arrays.copyOf(
+ EQUALIZER_PRESET_USER_BAND_LEVEL_DEFAULT, mEQNumBands);
+ // If no preset prefs set use CI EXTREME (= numPresets)
+ final short eQPreset = (short) prefs.getInt(Key.eq_current_preset.toString(),
+ mEQNumPresets);
+ editor.putInt(Key.eq_current_preset.toString(), eQPreset);
+ final short[] bandLevel = new short[mEQNumBands];
+ for (short band = 0; band < mEQNumBands; band++) {
+ if (eQPreset < mEQNumPresets) {
+ // OpenSL ES effect presets
+ bandLevel[band] = mEQPresetOpenSLESBandLevel[eQPreset][band];
+ } else if (eQPreset == mEQNumPresets) {
+ // CI EXTREME
+ bandLevel[band] = eQPresetCIExtremeBandLevel[band];
+ } else {
+ // User
+ bandLevel[band] = (short) prefs.getInt(
+ Key.eq_preset_user_band_level.toString() + band,
+ eQPresetUserBandLevelDefault[band]);
+ }
+ editor.putInt(Key.eq_band_level.toString() + band, bandLevel[band]);
+ editor.putInt(Key.eq_center_freq.toString() + band, mEQCenterFreq[band]);
+ editor.putInt(Key.eq_preset_ci_extreme_band_level.toString() + band,
+ eQPresetCIExtremeBandLevel[band]);
+ editor.putInt(Key.eq_preset_user_band_level_default.toString() + band,
+ eQPresetUserBandLevelDefault[band]);
+ }
+ for (short preset = 0; preset < mEQNumPresets; preset++) {
+ editor.putString(Key.eq_preset_name.toString() + preset, mEQPresetNames[preset]);
+ for (short band = 0; band < mEQNumBands; band++) {
+ editor.putInt(Key.eq_preset_opensl_es_band_level.toString() + preset + "_"
+ + band, mEQPresetOpenSLESBandLevel[preset][band]);
+ }
+ }
+ }
+ final boolean isEQEnabled = prefs.getBoolean(Key.eq_enabled.toString(),
+ EQUALIZER_ENABLED_DEFAULT);
+ editor.putBoolean(Key.eq_enabled.toString(), isEQEnabled);
+
+ // Preset reverb
+ final boolean isEnabledPR = prefs.getBoolean(Key.pr_enabled.toString(),
+ PRESET_REVERB_ENABLED_DEFAULT);
+ final short presetPR = (short) prefs.getInt(Key.pr_current_preset.toString(),
+ PRESET_REVERB_CURRENT_PRESET_DEFAULT);
+ editor.putBoolean(Key.pr_enabled.toString(), isEnabledPR);
+ editor.putInt(Key.pr_current_preset.toString(), presetPR);
+
+ editor.commit();
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "initEffectsPreferences: processingEnabled: " + e);
+ }
+ }
+
+ /**
+ * Gets the effect control mode based on the given audio session in the control panel. Control
+ * mode defines if the control panel is controlling effects and/or preferences
+ *
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @return effect control mode
+ */
+ public static ControlMode getControlMode(final int audioSession) {
+ if (audioSession == AudioEffect.ERROR_BAD_VALUE) {
+ return ControlMode.CONTROL_PREFERENCES;
+ }
+ return ControlMode.CONTROL_EFFECTS;
+ }
+
+ /**
+ * Sets boolean parameter to value for given key
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @param value
+ */
+ public static void setParameterBoolean(final Context context, final String packageName,
+ final int audioSession, final Key key, final boolean value) {
+ try {
+ final SharedPreferences prefs = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE);
+ final ControlMode controlMode = getControlMode(audioSession);
+ boolean enabled = value;
+
+ // Global on/off
+ if (key == Key.global_enabled) {
+ boolean processingEnabled = false;
+ if (value == true) {
+ // enable all with respect to preferences
+ if (controlMode == ControlMode.CONTROL_EFFECTS) {
+ final Virtualizer virtualizerEffect = getVirtualizerEffect(audioSession);
+ if (virtualizerEffect != null) {
+ virtualizerEffect.setEnabled(true);
+ int defaultstrength = virtualizerEffect.getRoundedStrength();
+ final int vIStrength = prefs.getInt(Key.virt_strength.toString(),
+ defaultstrength);
+ boolean on = prefs.getBoolean(Key.virt_enabled.toString(), VIRTUALIZER_ENABLED_DEFAULT);
+ if (on) {
+ setParameterInt(context, packageName,
+ audioSession, Key.virt_strength, vIStrength);
+ } else {
+ mPrevVirtStrength = vIStrength;
+ virtualizerEffect.setStrength((short) 0);
+ }
+ }
+ final BassBoost bassBoostEffect = getBassBoostEffect(audioSession);
+ if (bassBoostEffect != null) {
+ bassBoostEffect.setEnabled(true);
+ final int bBStrength = prefs.getInt(Key.bb_strength.toString(),
+ BASS_BOOST_STRENGTH_DEFAULT);
+ boolean on = prefs.getBoolean(Key.bb_enabled.toString(), BASS_BOOST_ENABLED_DEFAULT);
+ if (on) {
+ setParameterInt(context, packageName,
+ audioSession, Key.bb_strength, bBStrength);
+ } else {
+ mPrevBassBoostStrength = bBStrength;
+ bassBoostEffect.setStrength((short) 0);
+ }
+ }
+ final Equalizer equalizerEffect = getEqualizerEffect(audioSession);
+ if (equalizerEffect != null) {
+ equalizerEffect.setEnabled(prefs.getBoolean(Key.eq_enabled.toString(),
+ EQUALIZER_ENABLED_DEFAULT));
+ final int[] bandLevels = getParameterIntArray(context,
+ packageName, audioSession, Key.eq_band_level);
+ final int len = bandLevels.length;
+ for (short band = 0; band < len; band++) {
+ final int level = bandLevels[band];
+ setParameterInt(context, packageName,
+ audioSession, Key.eq_band_level_no_save, level, band);
+ }
+ }
+ // XXX: Preset Reverb not used for the moment, so commented out the effect
+ // creation to not use MIPS
+ final PresetReverb presetReverbEffect = getPresetReverbEffect(audioSession);
+ if (presetReverbEffect != null) {
+ presetReverbEffect.setEnabled(prefs.getBoolean(Key.pr_enabled.toString(),
+ PRESET_REVERB_ENABLED_DEFAULT));
+ final short presetPR = (short) prefs.getInt(Key.pr_current_preset.toString(),
+ PRESET_REVERB_CURRENT_PRESET_DEFAULT);
+ setParameterInt(context, packageName,
+ audioSession, Key.pr_current_preset, presetPR);
+ }
+ }
+
+ processingEnabled = true;
+ Log.v(TAG, "processingEnabled=" + processingEnabled);
+
+ } else {
+ // disable all
+ if (controlMode == ControlMode.CONTROL_EFFECTS) {
+ final Virtualizer virtualizerEffect = getVirtualizerEffectNoCreate(audioSession);
+ if (virtualizerEffect != null) {
+ mVirtualizerInstances.remove(audioSession, virtualizerEffect);
+ virtualizerEffect.setEnabled(false);
+ virtualizerEffect.release();
+ }
+ final BassBoost bassBoostEffect = getBassBoostEffectNoCreate(audioSession);
+ if (bassBoostEffect != null) {
+ mBassBoostInstances.remove(audioSession, bassBoostEffect);
+ bassBoostEffect.setEnabled(false);
+ bassBoostEffect.release();
+ }
+ final Equalizer equalizerEffect = getEqualizerEffectNoCreate(audioSession);
+ if (equalizerEffect != null) {
+ mEQInstances.remove(audioSession, equalizerEffect);
+ equalizerEffect.setEnabled(false);
+ equalizerEffect.release();
+ }
+ // XXX: Preset Reverb not used for the moment, so commented out the effect
+ // creation to not use MIPS
+ final PresetReverb presetReverbEffect = getPresetReverbEffect(audioSession);
+ if (presetReverbEffect != null) {
+ mPresetReverbInstances.remove(audioSession, presetReverbEffect);
+ presetReverbEffect.setEnabled(false);
+ presetReverbEffect.release();
+ }
+ }
+
+ processingEnabled = false;
+ Log.v(TAG, "processingEnabled=" + processingEnabled);
+ }
+ enabled = processingEnabled;
+ } else if (controlMode == ControlMode.CONTROL_EFFECTS) {
+ final boolean isGlobalEnabled = prefs.getBoolean(Key.global_enabled.toString(),
+ GLOBAL_ENABLED_DEFAULT);
+ if (isGlobalEnabled == true) {
+ // Set effect parameters
+ switch (key) {
+
+ case global_enabled:
+ // Global, already handled, to get out error free
+ break;
+
+ // Virtualizer
+ case virt_enabled:
+ final Virtualizer virtualizerEffect = getVirtualizerEffect(audioSession);
+ if (virtualizerEffect != null) {
+ if (value) {
+ virtualizerEffect.setStrength((short) mPrevVirtStrength);
+ enabled = true;
+ } else {
+ mPrevVirtStrength = virtualizerEffect.getRoundedStrength();
+ virtualizerEffect.setStrength((short) 0);
+ enabled = false;
+ }
+ }
+ break;
+
+ // BassBoost
+ case bb_enabled:
+ final BassBoost bassBoostEffect = getBassBoostEffect(audioSession);
+ if (bassBoostEffect != null) {
+ if (value) {
+ bassBoostEffect.setStrength((short) mPrevBassBoostStrength);
+ enabled = true;
+ } else {
+ mPrevBassBoostStrength = bassBoostEffect.getRoundedStrength();
+ bassBoostEffect.setStrength((short) 0);
+ enabled = false;
+ }
+ }
+ break;
+
+ // Equalizer
+ case eq_enabled:
+ final Equalizer equalizerEffect = getEqualizerEffect(audioSession);
+ if (equalizerEffect != null) {
+ equalizerEffect.setEnabled(value);
+ enabled = equalizerEffect.getEnabled();
+ }
+ break;
+
+ // PresetReverb
+ case pr_enabled:
+ // XXX: Preset Reverb not used for the moment, so commented out the effect
+ // creation to not use MIPS
+ final PresetReverb presetReverbEffect = getPresetReverbEffect(audioSession);
+ if (presetReverbEffect != null) {
+ presetReverbEffect.setEnabled(value);
+ enabled = presetReverbEffect.getEnabled();
+ }
+ break;
+
+ default:
+ Log.e(TAG, "Unknown/unsupported key " + key);
+ return;
+ }
+ }
+
+ }
+
+ // Set preferences
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(key.toString(), enabled);
+ editor.commit();
+
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "setParameterBoolean: " + key + "; " + value + "; " + e);
+ }
+ }
+
+ /**
+ * Gets boolean parameter for given key
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @return parameter value
+ */
+ public static Boolean getParameterBoolean(final Context context, final String packageName,
+ final int audioSession, final Key key) {
+ final SharedPreferences prefs = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE);
+ boolean value = false;
+
+ try {
+ value = prefs.getBoolean(key.toString(), value);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "getParameterBoolean: " + key + "; " + value + "; " + e);
+ }
+
+ return value;
+
+ }
+
+ /**
+ * Sets int parameter for given key and value arg0, arg1
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @param arg0
+ * @param arg1
+ */
+ public static void setParameterInt(final Context context, final String packageName,
+ final int audioSession, final Key key, final int arg0, final int arg1) {
+ String strKey = key.toString();
+ int value = arg0;
+
+ try {
+ final SharedPreferences prefs = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE);
+ final SharedPreferences.Editor editor = prefs.edit();
+ final ControlMode controlMode = getControlMode(audioSession);
+
+ // Set effect parameters
+ if (controlMode == ControlMode.CONTROL_EFFECTS) {
+
+ switch (key) {
+
+ // Virtualizer
+ case virt_strength: {
+ final Virtualizer virtualizerEffect = getVirtualizerEffect(audioSession);
+ boolean on = prefs.getBoolean(Key.virt_enabled.toString(), VIRTUALIZER_ENABLED_DEFAULT);
+ if ((virtualizerEffect != null) && on) {
+ virtualizerEffect.setStrength((short) value);
+ value = virtualizerEffect.getRoundedStrength();
+ }
+ break;
+ }
+ // BassBoost
+ case bb_strength: {
+ final BassBoost bassBoostEffect = getBassBoostEffect(audioSession);
+ boolean on = prefs.getBoolean(Key.bb_enabled.toString(), BASS_BOOST_ENABLED_DEFAULT);
+ if ((bassBoostEffect != null) && on) {
+ bassBoostEffect.setStrength((short) value);
+ value = bassBoostEffect.getRoundedStrength();
+ }
+ break;
+ }
+ // Equalizer
+ case eq_band_level: {
+ if (arg1 == DUMMY_ARGUMENT) {
+ throw new IllegalArgumentException("Dummy arg passed.");
+ }
+ final short band = (short) arg1;
+ strKey = strKey + band;
+ final Equalizer equalizerEffect = getEqualizerEffect(audioSession);
+ if (equalizerEffect != null) {
+ equalizerEffect.setBandLevel(band, (short) value);
+ value = equalizerEffect.getBandLevel(band);
+ // save band level in User preset
+ editor.putInt(Key.eq_preset_user_band_level.toString() + band, value);
+ }
+ break;
+ }
+ // Same as eq_band_level except won't save band level in User preset
+ case eq_band_level_no_save: {
+ if (arg1 == DUMMY_ARGUMENT) {
+ throw new IllegalArgumentException("Dummy arg passed.");
+ }
+ final short band = (short) arg1;
+ strKey = Key.eq_band_level.toString() + band;
+ final Equalizer equalizerEffect = getEqualizerEffect(audioSession);
+ if (equalizerEffect != null) {
+ equalizerEffect.setBandLevel(band, (short) value);
+ }
+ break;
+ }
+ case eq_current_preset: {
+ final Equalizer equalizerEffect = getEqualizerEffect(audioSession);
+ if (equalizerEffect != null) {
+ final short preset = (short) value;
+ final int numBands = prefs.getInt(Key.eq_num_bands.toString(),
+ EQUALIZER_NUMBER_BANDS_DEFAULT);
+ final int numPresets = prefs.getInt(Key.eq_num_presets.toString(),
+ EQUALIZER_NUMBER_PRESETS_DEFAULT);
+
+ if (preset < numPresets) {
+ // OpenSL ES EQ Effect presets
+ equalizerEffect.usePreset(preset);
+ value = equalizerEffect.getCurrentPreset();
+ } else {
+ final short[] eQPresetCIExtremeBandLevelDefault = Arrays.copyOf(
+ EQUALIZER_PRESET_CIEXTREME_BAND_LEVEL, numBands);
+ final short[] eQPresetUserBandLevelDefault = Arrays.copyOf(
+ EQUALIZER_PRESET_USER_BAND_LEVEL_DEFAULT, numBands);
+ // Set the band levels manually for custom presets
+ for (short band = 0; band < numBands; band++) {
+ short bandLevel = 0;
+ if (preset == numPresets) {
+ // CI EXTREME
+ bandLevel = (short) prefs.getInt(
+ Key.eq_preset_ci_extreme_band_level.toString() + band,
+ eQPresetCIExtremeBandLevelDefault[band]);
+ } else {
+ // User
+ bandLevel = (short) prefs.getInt(
+ Key.eq_preset_user_band_level.toString() + band,
+ eQPresetUserBandLevelDefault[band]);
+ }
+ equalizerEffect.setBandLevel(band, bandLevel);
+ }
+ }
+
+ // update band levels
+ for (short band = 0; band < numBands; band++) {
+ final short level = equalizerEffect.getBandLevel(band);
+ editor.putInt(Key.eq_band_level.toString() + band, level);
+ }
+ }
+ break;
+ }
+ case eq_preset_user_band_level:
+ // Fall through
+ case eq_preset_user_band_level_default:
+ // Fall through
+ case eq_preset_ci_extreme_band_level: {
+ if (arg1 == DUMMY_ARGUMENT) {
+ throw new IllegalArgumentException("Dummy arg passed.");
+ }
+ final short band = (short) arg1;
+ strKey = strKey + band;
+ break;
+ }
+ case pr_current_preset:
+ // XXX: Preset Reverb not used for the moment, so commented out the effect
+ // creation to not use MIPS
+ final PresetReverb presetReverbEffect = getPresetReverbEffect(audioSession);
+ if (presetReverbEffect != null) {
+ presetReverbEffect.setPreset((short) value);
+ value = presetReverbEffect.getPreset();
+ }
+ break;
+ default:
+ Log.e(TAG, "setParameterInt: Unknown/unsupported key " + key);
+ return;
+ }
+ } else {
+ switch (key) {
+ // Virtualizer
+ case virt_strength:
+ // Do nothing
+ break;
+ case virt_type:
+ // Do nothing
+ break;
+
+ // BassBoost
+ case bb_strength:
+ // Do nothing
+ break;
+
+ // Equalizer
+ case eq_band_level: {
+ if (arg1 == DUMMY_ARGUMENT) {
+ throw new IllegalArgumentException("Dummy arg passed.");
+ }
+ final short band = (short) arg1;
+ strKey = strKey + band;
+
+ editor.putInt(Key.eq_preset_user_band_level.toString() + band, value);
+ break;
+ }
+ case eq_band_level_no_save: {
+ if (arg1 == DUMMY_ARGUMENT) {
+ throw new IllegalArgumentException("Dummy arg passed.");
+ }
+ final short band = (short) arg1;
+ strKey = Key.eq_band_level.toString() + band;
+ break;
+ }
+ case eq_current_preset: {
+ final short preset = (short) value;
+ final int numBands = prefs.getInt(Key.eq_num_bands.toString(),
+ EQUALIZER_NUMBER_BANDS_DEFAULT);
+ final int numPresets = prefs.getInt(Key.eq_num_presets.toString(),
+ EQUALIZER_NUMBER_PRESETS_DEFAULT);
+
+ final short[][] eQPresetOpenSLESBandLevelDefault = Arrays.copyOf(
+ EQUALIZER_PRESET_OPENSL_ES_BAND_LEVEL_DEFAULT, numBands);
+ final short[] eQPresetCIExtremeBandLevelDefault = Arrays.copyOf(
+ EQUALIZER_PRESET_CIEXTREME_BAND_LEVEL, numBands);
+ final short[] eQPresetUserBandLevelDefault = Arrays.copyOf(
+ EQUALIZER_PRESET_USER_BAND_LEVEL_DEFAULT, numBands);
+ for (short band = 0; band < numBands; band++) {
+ short bandLevel = 0;
+ if (preset < numPresets) {
+ // OpenSL ES EQ Effect presets
+ bandLevel = (short) prefs.getInt(
+ Key.eq_preset_opensl_es_band_level.toString() + preset + "_"
+ + band, eQPresetOpenSLESBandLevelDefault[preset][band]);
+ } else if (preset == numPresets) {
+ // CI EXTREME
+ bandLevel = (short) prefs.getInt(
+ Key.eq_preset_ci_extreme_band_level.toString() + band,
+ eQPresetCIExtremeBandLevelDefault[band]);
+ } else {
+ // User
+ bandLevel = (short) prefs.getInt(
+ Key.eq_preset_user_band_level.toString() + band,
+ eQPresetUserBandLevelDefault[band]);
+ }
+ editor.putInt(Key.eq_band_level.toString() + band, bandLevel);
+ }
+ break;
+ }
+ case eq_preset_user_band_level:
+ // Fall through
+ case eq_preset_user_band_level_default:
+ // Fall through
+ case eq_preset_ci_extreme_band_level: {
+ if (arg1 == DUMMY_ARGUMENT) {
+ throw new IllegalArgumentException("Dummy arg passed.");
+ }
+ final short band = (short) arg1;
+ strKey = strKey + band;
+ break;
+ }
+ case pr_current_preset:
+ // Do nothing
+ break;
+ default:
+ Log.e(TAG, "setParameterInt: Unknown/unsupported key " + key);
+ return;
+ }
+ }
+
+ // Set preferences
+ editor.putInt(strKey, value);
+ editor.apply();
+
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "setParameterInt: " + key + "; " + arg0 + "; " + arg1 + "; " + e);
+ }
+
+ }
+
+ /**
+ * Sets int parameter for given key and value arg
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @param arg
+ */
+ public static void setParameterInt(final Context context, final String packageName,
+ final int audioSession, final Key key, final int arg) {
+ setParameterInt(context, packageName, audioSession, key, arg, DUMMY_ARGUMENT);
+ }
+
+ /**
+ * Gets int parameter given key
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @return parameter value
+ */
+ public static int getParameterInt(final Context context, final String packageName,
+ final int audioSession, final String key) {
+ int value = 0;
+
+ try {
+ final SharedPreferences prefs = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE);
+ value = prefs.getInt(key, value);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "getParameterInt: " + key + "; " + e);
+ }
+
+ return value;
+ }
+
+ /**
+ * Gets int parameter given key
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @return parameter value
+ */
+ public static int getParameterInt(final Context context, final String packageName,
+ final int audioSession, final Key key) {
+ return getParameterInt(context, packageName, audioSession, key.toString());
+ }
+
+ /**
+ * Gets int parameter given key and arg
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param audioSession
+ * @param key
+ * @param arg
+ * @return parameter value
+ */
+ public static int getParameterInt(final Context context, final String packageName,
+ final int audioSession, final Key key, final int arg) {
+ return getParameterInt(context, packageName, audioSession, key.toString() + arg);
+ }
+
+ /**
+ * Gets int parameter given key, arg0 and arg1
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param audioSession
+ * @param key
+ * @param arg0
+ * @param arg1
+ * @return parameter value
+ */
+ public static int getParameterInt(final Context context, final String packageName,
+ final int audioSession, final Key key, final int arg0, final int arg1) {
+ return getParameterInt(context, packageName, audioSession, key.toString() + arg0 + "_"
+ + arg1);
+ }
+
+ /**
+ * Gets integer array parameter given key. Returns null if not found.
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @return parameter value array
+ */
+ public static int[] getParameterIntArray(final Context context, final String packageName,
+ final int audioSession, final Key key) {
+ final SharedPreferences prefs = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE);
+
+ int[] intArray = null;
+ try {
+ // Get effect parameters
+ switch (key) {
+ case eq_level_range: {
+ intArray = new int[2];
+ break;
+ }
+ case eq_center_freq:
+ // Fall through
+ case eq_band_level:
+ // Fall through
+ case eq_preset_user_band_level:
+ // Fall through
+ case eq_preset_user_band_level_default:
+ // Fall through
+ case eq_preset_ci_extreme_band_level: {
+ final int numBands = prefs.getInt(Key.eq_num_bands.toString(), 0);
+ intArray = new int[numBands];
+ break;
+ }
+ default:
+ Log.e(TAG, "getParameterIntArray: Unknown/unsupported key " + key);
+ return null;
+ }
+
+ for (int i = 0; i < intArray.length; i++) {
+ intArray[i] = prefs.getInt(key.toString() + i, 0);
+ }
+
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "getParameterIntArray: " + key + "; " + e);
+ }
+
+ return intArray;
+ }
+
+ /**
+ * Gets string parameter given key. Returns empty string if not found.
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @return parameter value
+ */
+ public static String getParameterString(final Context context, final String packageName,
+ final int audioSession, final String key) {
+ String value = "";
+ try {
+ final SharedPreferences prefs = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE);
+
+ // Get effect parameters
+ value = prefs.getString(key, value);
+
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "getParameterString: " + key + "; " + e);
+ }
+
+ return value;
+ }
+
+ /**
+ * Gets string parameter given key.
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param key
+ * @return parameter value
+ */
+ public static String getParameterString(final Context context, final String packageName,
+ final int audioSession, final Key key) {
+ return getParameterString(context, packageName, audioSession, key.toString());
+ }
+
+ /**
+ * Gets string parameter given key and arg.
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param args
+ * @return parameter value
+ */
+ public static String getParameterString(final Context context, final String packageName,
+ final int audioSession, final Key key, final int arg) {
+ return getParameterString(context, packageName, audioSession, key.toString() + arg);
+ }
+
+ /**
+ * Opens/initializes the effects session for the given audio session with preferences linked to
+ * the given package name and context.
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ */
+ public static void openSession(final Context context, final String packageName,
+ final int audioSession) {
+ Log.v(TAG, "openSession(" + context + ", " + packageName + ", " + audioSession + ")");
+ final String methodTag = "openSession: ";
+
+ // init preferences
+ final SharedPreferences prefs = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE);
+ final SharedPreferences.Editor editor = prefs.edit();
+
+ final boolean isGlobalEnabled = prefs.getBoolean(Key.global_enabled.toString(),
+ GLOBAL_ENABLED_DEFAULT);
+ editor.putBoolean(Key.global_enabled.toString(), isGlobalEnabled);
+
+ if (!isGlobalEnabled) {
+ return;
+ }
+
+ // Manage audioSession information
+
+ // Retrieve AudioSession Id from map
+ boolean isExistingAudioSession = false;
+
+ try {
+ final Integer currentAudioSession = mPackageSessions.putIfAbsent(packageName,
+ audioSession);
+ if (currentAudioSession != null) {
+ // Compare with passed argument
+ if (currentAudioSession == audioSession) {
+ // FIXME: Normally, we should exit the function here
+ // BUT: we have to take care of the virtualizer because of
+ // a bug in the Android Effects Framework
+ // editor.commit();
+ // return;
+ isExistingAudioSession = true;
+ } else {
+ closeSession(context, packageName, currentAudioSession);
+ }
+ }
+ } catch (final NullPointerException e) {
+ Log.e(TAG, methodTag + e);
+ editor.commit();
+ return;
+ }
+
+ // Because the audioSession is new, get effects & settings from shared preferences
+
+ // Virtualizer
+ // create effect
+ final Virtualizer virtualizerEffect = getVirtualizerEffect(audioSession);
+ {
+ final String errorTag = methodTag + "Virtualizer error: ";
+
+ try {
+ // read parameters
+ final boolean isEnabled = prefs.getBoolean(Key.virt_enabled.toString(),
+ VIRTUALIZER_ENABLED_DEFAULT);
+ int defaultstrength = isExistingAudioSession ? VIRTUALIZER_STRENGTH_DEFAULT :
+ virtualizerEffect.getRoundedStrength();
+ final int strength = prefs.getInt(Key.virt_strength.toString(), defaultstrength);
+ // init settings
+ Virtualizer.Settings settings = new Virtualizer.Settings("Virtualizer;strength="
+ + strength);
+
+ virtualizerEffect.setProperties(settings);
+
+ // set parameters
+ if (isGlobalEnabled == true) {
+ virtualizerEffect.setEnabled(isEnabled);
+ } else {
+ virtualizerEffect.setEnabled(false);
+ }
+
+ // get parameters
+ settings = virtualizerEffect.getProperties();
+ Log.v(TAG, "Parameters: " + settings.toString() + ";enabled=" + isEnabled);
+
+ // update preferences
+ editor.putBoolean(Key.virt_enabled.toString(), isEnabled);
+ editor.putInt(Key.virt_strength.toString(), settings.strength);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, errorTag + e);
+ }
+ }
+
+ // In case of an existing audio session
+ // Exit after the virtualizer has been re-enabled
+
+ if (isExistingAudioSession) {
+ editor.apply();
+ return;
+ }
+
+ // BassBoost
+ // create effect
+ final BassBoost bassBoostEffect = getBassBoostEffect(audioSession);
+ {
+ final String errorTag = methodTag + "BassBoost error: ";
+
+ try {
+ // read parameters
+ final boolean isEnabled = prefs.getBoolean(Key.bb_enabled.toString(),
+ BASS_BOOST_ENABLED_DEFAULT);
+ final int strength = prefs.getInt(Key.bb_strength.toString(),
+ BASS_BOOST_STRENGTH_DEFAULT);
+
+ // init settings
+ BassBoost.Settings settings = new BassBoost.Settings("BassBoost;strength="
+ + strength);
+
+ bassBoostEffect.setProperties(settings);
+
+ // set parameters
+ if (isGlobalEnabled == true) {
+ bassBoostEffect.setEnabled(isEnabled);
+ } else {
+ bassBoostEffect.setEnabled(false);
+ }
+
+ // get parameters
+ settings = bassBoostEffect.getProperties();
+ Log.v(TAG, "Parameters: " + settings.toString() + ";enabled=" + isEnabled);
+
+ // update preferences
+ editor.putBoolean(Key.bb_enabled.toString(), isEnabled);
+ editor.putInt(Key.bb_strength.toString(), settings.strength);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, errorTag + e);
+ }
+ }
+
+ // Equalizer
+ // create effect
+ final Equalizer equalizerEffect = getEqualizerEffect(audioSession);
+ {
+ final String errorTag = methodTag + "Equalizer error: ";
+
+ try {
+ final short eQNumBands;
+ final short[] bandLevel;
+ final int[] eQCenterFreq;
+ final short eQNumPresets;
+ final String[] eQPresetNames;
+ short eQPreset;
+ synchronized (mEQInitLock) {
+ // read parameters
+ mEQBandLevelRange = equalizerEffect.getBandLevelRange();
+ mEQNumBands = equalizerEffect.getNumberOfBands();
+ mEQCenterFreq = new int[mEQNumBands];
+ mEQNumPresets = equalizerEffect.getNumberOfPresets();
+ mEQPresetNames = new String[mEQNumPresets];
+
+ for (short preset = 0; preset < mEQNumPresets; preset++) {
+ mEQPresetNames[preset] = equalizerEffect.getPresetName(preset);
+ editor.putString(Key.eq_preset_name.toString() + preset,
+ mEQPresetNames[preset]);
+ }
+
+ editor.putInt(Key.eq_level_range.toString() + 0, mEQBandLevelRange[0]);
+ editor.putInt(Key.eq_level_range.toString() + 1, mEQBandLevelRange[1]);
+ editor.putInt(Key.eq_num_bands.toString(), mEQNumBands);
+ editor.putInt(Key.eq_num_presets.toString(), mEQNumPresets);
+ // Resetting the EQ arrays depending on the real # bands with defaults if band <
+ // default size else 0 by copying default arrays over new ones
+ final short[] eQPresetCIExtremeBandLevel = Arrays.copyOf(
+ EQUALIZER_PRESET_CIEXTREME_BAND_LEVEL, mEQNumBands);
+ final short[] eQPresetUserBandLevelDefault = Arrays.copyOf(
+ EQUALIZER_PRESET_USER_BAND_LEVEL_DEFAULT, mEQNumBands);
+ // If no preset prefs set use CI EXTREME (= numPresets)
+ eQPreset = (short) prefs
+ .getInt(Key.eq_current_preset.toString(), mEQNumPresets);
+ if (eQPreset < mEQNumPresets) {
+ // OpenSL ES effect presets
+ equalizerEffect.usePreset(eQPreset);
+ eQPreset = equalizerEffect.getCurrentPreset();
+ } else {
+ for (short band = 0; band < mEQNumBands; band++) {
+ short level = 0;
+ if (eQPreset == mEQNumPresets) {
+ // CI EXTREME
+ level = eQPresetCIExtremeBandLevel[band];
+ } else {
+ // User
+ level = (short) prefs.getInt(
+ Key.eq_preset_user_band_level.toString() + band,
+ eQPresetUserBandLevelDefault[band]);
+ }
+ equalizerEffect.setBandLevel(band, level);
+ }
+ }
+ editor.putInt(Key.eq_current_preset.toString(), eQPreset);
+
+ bandLevel = new short[mEQNumBands];
+ for (short band = 0; band < mEQNumBands; band++) {
+ mEQCenterFreq[band] = equalizerEffect.getCenterFreq(band);
+ bandLevel[band] = equalizerEffect.getBandLevel(band);
+
+ editor.putInt(Key.eq_band_level.toString() + band, bandLevel[band]);
+ editor.putInt(Key.eq_center_freq.toString() + band, mEQCenterFreq[band]);
+ editor.putInt(Key.eq_preset_ci_extreme_band_level.toString() + band,
+ eQPresetCIExtremeBandLevel[band]);
+ editor.putInt(Key.eq_preset_user_band_level_default.toString() + band,
+ eQPresetUserBandLevelDefault[band]);
+ }
+
+ eQNumBands = mEQNumBands;
+ eQCenterFreq = mEQCenterFreq;
+ eQNumPresets = mEQNumPresets;
+ eQPresetNames = mEQPresetNames;
+ }
+
+ final boolean isEnabled = prefs.getBoolean(Key.eq_enabled.toString(),
+ EQUALIZER_ENABLED_DEFAULT);
+ editor.putBoolean(Key.eq_enabled.toString(), isEnabled);
+ if (isGlobalEnabled == true) {
+ equalizerEffect.setEnabled(isEnabled);
+ } else {
+ equalizerEffect.setEnabled(false);
+ }
+
+ // dump
+ Log.v(TAG, "Parameters: Equalizer");
+ Log.v(TAG, "bands=" + eQNumBands);
+ String str = "levels=";
+ for (short band = 0; band < eQNumBands; band++) {
+ str = str + bandLevel[band] + "; ";
+ }
+ Log.v(TAG, str);
+ str = "center=";
+ for (short band = 0; band < eQNumBands; band++) {
+ str = str + eQCenterFreq[band] + "; ";
+ }
+ Log.v(TAG, str);
+ str = "presets=";
+ for (short preset = 0; preset < eQNumPresets; preset++) {
+ str = str + eQPresetNames[preset] + "; ";
+ }
+ Log.v(TAG, str);
+ Log.v(TAG, "current=" + eQPreset);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, errorTag + e);
+ }
+ }
+
+ // XXX: Preset Reverb not used for the moment, so commented out the effect creation to not
+ // use MIPS left in the code for (future) reference.
+ // Preset reverb
+ // create effect
+ final PresetReverb presetReverbEffect = getPresetReverbEffect(audioSession);
+ {
+ final String errorTag = methodTag + "PresetReverb error: ";
+
+ try {
+ // read parameters
+ final boolean isEnabled = prefs.getBoolean(Key.pr_enabled.toString(), PRESET_REVERB_ENABLED_DEFAULT);
+ final short preset = (short) prefs.getInt(Key.pr_current_preset.toString(), PRESET_REVERB_CURRENT_PRESET_DEFAULT);
+
+ // init settings
+ PresetReverb.Settings settings = new PresetReverb.Settings("PresetReverb;preset=" + preset);
+
+ // read/update preferences
+ presetReverbEffect.setProperties(settings);
+
+ // set parameters
+ if (isGlobalEnabled == true) {
+ presetReverbEffect.setEnabled(isEnabled);
+ } else {
+ presetReverbEffect.setEnabled(false);
+ }
+
+ // get parameters
+ settings = presetReverbEffect.getProperties();
+ Log.v(TAG, "Parameters: " + settings.toString() + ";enabled=" + isEnabled);
+
+ // update preferences
+ editor.putBoolean(Key.pr_enabled.toString(), isEnabled);
+ editor.putInt(Key.pr_current_preset.toString(), settings.preset);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, errorTag + e);
+ }
+ }
+ editor.commit();
+ }
+
+ /**
+ * Closes the audio session (release effects) for the given session
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ */
+ public static void closeSession(final Context context, final String packageName,
+ final int audioSession) {
+ Log.v(TAG, "closeSession(" + context + ", " + packageName + ", " + audioSession + ")");
+
+ // PresetReverb
+ final PresetReverb presetReverb = mPresetReverbInstances.remove(audioSession);
+ if (presetReverb != null) {
+ presetReverb.release();
+ }
+ // Equalizer
+ final Equalizer equalizer = mEQInstances.remove(audioSession);
+ if (equalizer != null) {
+ equalizer.release();
+ }
+ // BassBoost
+ final BassBoost bassBoost = mBassBoostInstances.remove(audioSession);
+ if (bassBoost != null) {
+ bassBoost.release();
+ }
+ // Virtualizer
+ final Virtualizer virtualizer = mVirtualizerInstances.remove(audioSession);
+ if (virtualizer != null) {
+ virtualizer.release();
+ }
+
+ mPackageSessions.remove(packageName);
+ }
+
+ /**
+ * Enables or disables all effects (global enable/disable) for a given context, package name and
+ * audio session. It sets/inits the control mode and preferences and then sets the global
+ * enabled parameter.
+ *
+ * @param context
+ * @param packageName
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @param enabled
+ */
+ public static void setEnabledAll(final Context context, final String packageName,
+ final int audioSession, final boolean enabled) {
+ initEffectsPreferences(context, packageName, audioSession);
+ setParameterBoolean(context, packageName, audioSession, Key.global_enabled, enabled);
+ }
+
+ /**
+ * Gets the virtualizer effect for the given audio session. If the effect on the session doesn't
+ * exist yet, create it and add to collection.
+ *
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @return virtualizerEffect
+ */
+ private static Virtualizer getVirtualizerEffectNoCreate(final int audioSession) {
+ return mVirtualizerInstances.get(audioSession);
+ }
+ private static Virtualizer getVirtualizerEffect(final int audioSession) {
+ Virtualizer virtualizerEffect = getVirtualizerEffectNoCreate(audioSession);
+ if (virtualizerEffect == null) {
+ try {
+ final Virtualizer newVirtualizerEffect = new Virtualizer(PRIORITY, audioSession);
+ virtualizerEffect = mVirtualizerInstances.putIfAbsent(audioSession,
+ newVirtualizerEffect);
+ if (virtualizerEffect == null) {
+ // put succeeded, use new value
+ virtualizerEffect = newVirtualizerEffect;
+ }
+ } catch (final IllegalArgumentException e) {
+ Log.e(TAG, "Virtualizer: " + e);
+ } catch (final UnsupportedOperationException e) {
+ Log.e(TAG, "Virtualizer: " + e);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "Virtualizer: " + e);
+ }
+ }
+ return virtualizerEffect;
+ }
+
+ /**
+ * Gets the bass boost effect for the given audio session. If the effect on the session doesn't
+ * exist yet, create it and add to collection.
+ *
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @return bassBoostEffect
+ */
+ private static BassBoost getBassBoostEffectNoCreate(final int audioSession) {
+ return mBassBoostInstances.get(audioSession);
+ }
+ private static BassBoost getBassBoostEffect(final int audioSession) {
+
+ BassBoost bassBoostEffect = getBassBoostEffectNoCreate(audioSession);
+ if (bassBoostEffect == null) {
+ try {
+ final BassBoost newBassBoostEffect = new BassBoost(PRIORITY, audioSession);
+ bassBoostEffect = mBassBoostInstances.putIfAbsent(audioSession, newBassBoostEffect);
+ if (bassBoostEffect == null) {
+ // put succeeded, use new value
+ bassBoostEffect = newBassBoostEffect;
+ }
+ } catch (final IllegalArgumentException e) {
+ Log.e(TAG, "BassBoost: " + e);
+ } catch (final UnsupportedOperationException e) {
+ Log.e(TAG, "BassBoost: " + e);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "BassBoost: " + e);
+ }
+ }
+ return bassBoostEffect;
+ }
+
+ /**
+ * Gets the equalizer effect for the given audio session. If the effect on the session doesn't
+ * exist yet, create it and add to collection.
+ *
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @return equalizerEffect
+ */
+ private static Equalizer getEqualizerEffectNoCreate(final int audioSession) {
+ return mEQInstances.get(audioSession);
+ }
+ private static Equalizer getEqualizerEffect(final int audioSession) {
+ Equalizer equalizerEffect = getEqualizerEffectNoCreate(audioSession);
+ if (equalizerEffect == null) {
+ try {
+ final Equalizer newEqualizerEffect = new Equalizer(PRIORITY, audioSession);
+ equalizerEffect = mEQInstances.putIfAbsent(audioSession, newEqualizerEffect);
+ if (equalizerEffect == null) {
+ // put succeeded, use new value
+ equalizerEffect = newEqualizerEffect;
+ }
+ } catch (final IllegalArgumentException e) {
+ Log.e(TAG, "Equalizer: " + e);
+ } catch (final UnsupportedOperationException e) {
+ Log.e(TAG, "Equalizer: " + e);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "Equalizer: " + e);
+ }
+ }
+ return equalizerEffect;
+ }
+
+ // XXX: Preset Reverb not used for the moment, so commented out the effect creation to not
+ // use MIPS
+ /**
+ * Gets the preset reverb effect for the given audio session. If the effect on the session
+ * doesn't exist yet, create it and add to collection.
+ *
+ * @param audioSession
+ * System wide unique audio session identifier.
+ * @return presetReverbEffect
+ */
+ private static PresetReverb getPresetReverbEffect(final int audioSession) {
+ PresetReverb presetReverbEffect = mPresetReverbInstances.get(audioSession);
+ if (presetReverbEffect == null) {
+ try {
+ final PresetReverb newPresetReverbEffect = new PresetReverb(PRIORITY, audioSession);
+ presetReverbEffect = mPresetReverbInstances.putIfAbsent(audioSession,
+ newPresetReverbEffect);
+ if (presetReverbEffect == null) {
+ // put succeeded, use new value
+ presetReverbEffect = newPresetReverbEffect;
+ }
+ } catch (final IllegalArgumentException e) {
+ Log.e(TAG, "PresetReverb: " + e);
+ } catch (final UnsupportedOperationException e) {
+ Log.e(TAG, "PresetReverb: " + e);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "PresetReverb: " + e);
+ }
+ }
+ return presetReverbEffect;
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/ControlPanelPicker.java b/src/org/cyanogenmod/audiofx/ControlPanelPicker.java
new file mode 100644
index 0000000..1c6eaaf
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/ControlPanelPicker.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2011 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 org.cyanogenmod.audiofx;
+
+import com.android.internal.app.AlertActivity;
+import com.android.internal.app.AlertController;
+import com.android.internal.app.AlertController.AlertParams.OnPrepareListViewListener;
+import org.cyanogenmod.audiofx.Compatibility.Service;
+
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.media.audiofx.AudioEffect;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ListView;
+
+import java.util.List;
+
+/**
+ * shows a dialog that lets the user switch between control panels
+ */
+public class ControlPanelPicker extends AlertActivity implements OnClickListener, OnPrepareListViewListener {
+
+
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String [] cols = new String [] { "_id", "title", "package", "name" };
+ MatrixCursor c = new MatrixCursor(cols);
+
+ PackageManager pmgr = getPackageManager();
+ Intent i = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
+ List<ResolveInfo> ris = pmgr.queryIntentActivities(i, PackageManager.GET_DISABLED_COMPONENTS);
+ SharedPreferences pref = getSharedPreferences("musicfx", MODE_PRIVATE);
+ String savedDefPackage = pref.getString("defaultpanelpackage", null);
+ String savedDefName = pref.getString("defaultpanelname", null);
+ int cnt = -1;
+ int defpanelidx = 0;
+ for (ResolveInfo foo: ris) {
+ if (foo.activityInfo.name.equals(Compatibility.Redirector.class.getName())) {
+ continue;
+ }
+ CharSequence name = pmgr.getApplicationLabel(foo.activityInfo.applicationInfo);
+ c.addRow(new Object [] { 0, name, foo.activityInfo.packageName, foo.activityInfo.name });
+ cnt += 1;
+ if (foo.activityInfo.name.equals(savedDefName) &&
+ foo.activityInfo.packageName.equals(savedDefPackage) &&
+ foo.activityInfo.enabled) {
+ // mark as default in the list
+ defpanelidx = cnt;
+ }
+ }
+
+ final AlertController.AlertParams p = mAlertParams;
+ p.mCursor = c;
+ p.mOnClickListener = mItemClickListener;
+ p.mLabelColumn = "title";
+ p.mIsSingleChoice = true;
+ p.mPositiveButtonText = getString(com.android.internal.R.string.ok);
+ p.mPositiveButtonListener = this;
+ p.mNegativeButtonText = getString(com.android.internal.R.string.cancel);
+ p.mOnPrepareListViewListener = this;
+ p.mTitle = getString(R.string.picker_title);
+ p.mCheckedItem = defpanelidx;
+
+ setupAlert();
+ }
+
+ private DialogInterface.OnClickListener mItemClickListener =
+ new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int which) {
+ // Save the position of most recently clicked item
+ mAlertParams.mCheckedItem = which;
+ }
+
+ };
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ // set new default
+ Intent updateIntent = new Intent(this, Service.class);
+ Cursor c = mAlertParams.mCursor;
+ c.moveToPosition(mAlertParams.mCheckedItem);
+ updateIntent.putExtra("defPackage", c.getString(2));
+ updateIntent.putExtra("defName", c.getString(3));
+ startService(updateIntent);
+ }
+ }
+
+ @Override
+ public void onPrepareListView(ListView listView) {
+ //mAlertParams.mCheckedItem = mDefPanelPos;
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/ControlPanelReceiver.java b/src/org/cyanogenmod/audiofx/ControlPanelReceiver.java
new file mode 100644
index 0000000..4c693ec
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/ControlPanelReceiver.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2010-2011 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 org.cyanogenmod.audiofx;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.audiofx.AudioEffect;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.HashMap;
+
+public class ControlPanelReceiver extends BroadcastReceiver {
+
+ private final static String TAG = "AudioFXControlPanelReceiver";
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+
+ Log.v(TAG, "onReceive");
+
+ if ((context == null) || (intent == null)) {
+ Log.w(TAG, "Context or intent is null. Do nothing.");
+ return;
+ }
+
+ final String action = intent.getAction();
+ final String packageName = intent.getStringExtra(AudioEffect.EXTRA_PACKAGE_NAME);
+ final int audioSession = intent.getIntExtra(AudioEffect.EXTRA_AUDIO_SESSION,
+ AudioEffect.ERROR_BAD_VALUE);
+
+ Log.v(TAG, "Action: " + action);
+ Log.v(TAG, "Package name: " + packageName);
+ Log.v(TAG, "Audio session: " + audioSession);
+
+ // check package name
+ if (packageName == null) {
+ Log.w(TAG, "Null package name");
+ return;
+ }
+
+ // check audio session
+ if ((audioSession == AudioEffect.ERROR_BAD_VALUE) || (audioSession < 0)) {
+ Log.w(TAG, "Invalid or missing audio session " + audioSession);
+ return;
+ }
+
+ // open audio session
+ if (action.equals(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)) {
+
+ // retrieve the effect enabled state
+ final boolean isGlobalEnabled = context.getSharedPreferences(packageName,
+ Context.MODE_PRIVATE).getBoolean(
+ ControlPanelEffect.Key.global_enabled.toString(),
+ ControlPanelEffect.GLOBAL_ENABLED_DEFAULT);
+
+ ControlPanelEffect.openSession(context, packageName, audioSession);
+ }
+
+ // close audio session
+ if (action.equals(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)) {
+
+ ControlPanelEffect.closeSession(context, packageName, audioSession);
+ }
+
+ // set params
+ if (action.equals("AudioEffect.ACTION_SET_PARAM")) {
+ final String param = intent.getStringExtra("AudioEffect.EXTRA_PARAM");
+
+ if (param.equals("GLOBAL_ENABLED")) {
+ final Boolean value = intent.getBooleanExtra("AudioEffect.EXTRA_VALUE", false);
+ ControlPanelEffect.setParameterBoolean(context, packageName, audioSession,
+ ControlPanelEffect.Key.global_enabled, value);
+ }
+ }
+
+ // get params
+ if (action.equals("AudioEffect.ACTION_GET_PARAM")) {
+ final String param = intent.getStringExtra("AudioEffect.EXTRA_PARAM");
+
+ if (param.equals("GLOBAL_ENABLED")) {
+ final Boolean value = ControlPanelEffect.getParameterBoolean(context, packageName,
+ audioSession, ControlPanelEffect.Key.global_enabled);
+ final Bundle extras = new Bundle();
+ extras.putBoolean("GLOBAL_ENABLED", value);
+ setResultExtras(extras);
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/OpenSLESConstants.java b/src/org/cyanogenmod/audiofx/OpenSLESConstants.java
new file mode 100644
index 0000000..2131b55
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/OpenSLESConstants.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010-2011 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 org.cyanogenmod.audiofx;
+
+/**
+ * OpenSL ES constants class
+ */
+public final class OpenSLESConstants {
+ private OpenSLESConstants() {
+ // Empty constructor
+ }
+
+ /**
+ * Minimum volume level in millibel (mb).
+ */
+ public static final short SL_MILLIBEL_MIN = -9600;
+ /**
+ * This value is used when equalizer setting is not defined.
+ */
+ public static final short SL_EQUALIZER_UNDEFINED = (short) 0xFFFF;
+
+ /**
+ * The minimum bass boost strength in o/oo.
+ */
+ public static final short BASSBOOST_MIN_STRENGTH = 0;
+ /**
+ * The maximum bass boost strength in o/oo.
+ */
+ public static final short BASSBOOST_MAX_STRENGTH = 1000;
+
+ /**
+ * The minimum reverb room level in mb.
+ */
+ public static final short REVERB_MIN_ROOM_LEVEL = SL_MILLIBEL_MIN;
+ /**
+ * The maximum reverb room level in mb.
+ */
+ public static final short REVERB_MAX_ROOM_LEVEL = 0;
+ /**
+ * The minimum reverb room HF level in mb.
+ */
+ public static final short REVERB_MIN_ROOM_HF_LEVEL = SL_MILLIBEL_MIN;
+ /**
+ * The maximum reverb room HF level in mb.
+ */
+ public static final short REVERB_MAX_ROOM_HF_LEVEL = 0;
+ /**
+ * The minimum reverb decay time in ms.
+ */
+ public static final short REVERB_MIN_DECAY_TIME = 100;
+ /**
+ * The maximum reverb decay time in ms.
+ */
+ // XXX: OpenSL ES is normally 20000 but can only support 7000 for now
+ public static final short REVERB_MAX_DECAY_TIME = 7000;
+ /**
+ * The minimum reverb decay HF ratio in o/oo.
+ */
+ public static final short REVERB_MIN_DECAY_HF_RATIO = 100;
+ /**
+ * The maximum reverb decay HF ratio in o/oo.
+ */
+ public static final short REVERB_MAX_DECAY_HF_RATIO = 2000;
+ /**
+ * The minimum reverb level in mb.
+ */
+ public static final short REVERB_MIN_REVERB_LEVEL = SL_MILLIBEL_MIN;
+ /**
+ * The maximum reverb level in mb.
+ */
+ public static final short REVERB_MAX_REVERB_LEVEL = 2000;
+ /**
+ * The minimum reverb diffusion in o/oo.
+ */
+ public static final short REVERB_MIN_DIFFUSION = 0;
+ /**
+ * The maximum reverb diffusion in o/oo.
+ */
+ public static final short REVERB_MAX_DIFFUSION = 1000;
+ /**
+ * The minimum reverb density in o/oo.
+ */
+ public static final short REVERB_MIN_DENSITY = 0;
+ /**
+ * The maximum reverb density in o/oo.
+ */
+ public static final short REVERB_MAX_DENSITY = 1000;
+
+ /**
+ * The minimum virtualizer strength in o/oo.
+ */
+ public static final short VIRTUALIZER_MIN_STRENGTH = 0;
+ /**
+ * The maximum virtualizer strength in o/oo.
+ */
+ public static final short VIRTUALIZER_MAX_STRENGTH = 1000;
+
+ /**
+ * The minimum volume effect level in millibel (mb).
+ */
+ public static final short VOLUME_MIN_LEVEL = SL_MILLIBEL_MIN;
+ /**
+ * The minimum volume stereo position in o/oo.
+ */
+ public static final short VOLUME_MIN_STEREO_POSITION = -1000;
+ /**
+ * The maximum volume stereo position in o/oo.
+ */
+ public static final short VOLUME_MAX_STEREO_POSITION = 1000;
+}
diff --git a/src/org/cyanogenmod/audiofx/seekbar/AbsSeekBar.java b/src/org/cyanogenmod/audiofx/seekbar/AbsSeekBar.java
new file mode 100644
index 0000000..62a10a7
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/seekbar/AbsSeekBar.java
@@ -0,0 +1,561 @@
+/*
+ * Copyright (C) 2007 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 org.cyanogenmod.audiofx.seekbar;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+public abstract class AbsSeekBar extends ProgressBar {
+ private Drawable mThumb;
+ private int mThumbOffset;
+
+ /**
+ * On touch, this offset plus the scaled value from the position of the
+ * touch will form the progress value. Usually 0.
+ */
+ float mTouchProgressOffset;
+
+ /**
+ * Whether this is user seekable.
+ */
+ boolean mIsUserSeekable = true;
+
+ boolean mIsVertical = false;
+ /**
+ * On key presses (right or left), the amount to increment/decrement the
+ * progress.
+ */
+ private int mKeyProgressIncrement = 1;
+
+ private static final int NO_ALPHA = 0xFF;
+ private float mDisabledAlpha;
+
+ private int mScaledTouchSlop;
+ private float mTouchDownX;
+ private float mTouchDownY;
+ private boolean mIsDragging;
+
+ public AbsSeekBar(Context context) {
+ super(context);
+ }
+
+ public AbsSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.SeekBar, defStyle, 0);
+ Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb);
+ setThumb(thumb); // will guess mThumbOffset if thumb != null...
+ // ...but allow layout to override this
+ int thumbOffset = a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset());
+ setThumbOffset(thumbOffset);
+ a.recycle();
+
+ a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.Theme, 0, 0);
+ mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f);
+ a.recycle();
+
+ mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ }
+
+ /**
+ * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar.
+ * <p>
+ * If the thumb is a valid drawable (i.e. not null), half its width will be
+ * used as the new thumb offset (@see #setThumbOffset(int)).
+ *
+ * @param thumb Drawable representing the thumb
+ */
+ public void setThumb(Drawable thumb) {
+ boolean needUpdate;
+ // This way, calling setThumb again with the same bitmap will result in
+ // it recalcuating mThumbOffset (if for example it the bounds of the
+ // drawable changed)
+ if (mThumb != null && thumb != mThumb) {
+ mThumb.setCallback(null);
+ needUpdate = true;
+ } else {
+ needUpdate = false;
+ }
+ if (thumb != null) {
+ thumb.setCallback(this);
+
+ // Assuming the thumb drawable is symmetric, set the thumb offset
+ // such that the thumb will hang halfway off either edge of the
+ // progress bar.
+ if (mIsVertical) {
+ mThumbOffset = thumb.getIntrinsicHeight() / 2;
+ } else {
+ mThumbOffset = thumb.getIntrinsicWidth() / 2;
+ }
+
+ // If we're updating get the new states
+ if (needUpdate &&
+ (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth()
+ || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) {
+ requestLayout();
+ }
+ }
+ mThumb = thumb;
+ invalidate();
+ if (needUpdate) {
+ updateThumbPos(getWidth(), getHeight());
+ if (thumb.isStateful()) {
+ // Note that if the states are different this won't work.
+ // For now, let's consider that an app bug.
+ int[] state = getDrawableState();
+ thumb.setState(state);
+ }
+ }
+ }
+
+ /**
+ * @see #setThumbOffset(int)
+ */
+ public int getThumbOffset() {
+ return mThumbOffset;
+ }
+
+ /**
+ * Sets the thumb offset that allows the thumb to extend out of the range of
+ * the track.
+ *
+ * @param thumbOffset The offset amount in pixels.
+ */
+ public void setThumbOffset(int thumbOffset) {
+ mThumbOffset = thumbOffset;
+ invalidate();
+ }
+
+ /**
+ * Sets the amount of progress changed via the arrow keys.
+ *
+ * @param increment The amount to increment or decrement when the user
+ * presses the arrow keys.
+ */
+ public void setKeyProgressIncrement(int increment) {
+ mKeyProgressIncrement = increment < 0 ? -increment : increment;
+ }
+
+ /**
+ * Returns the amount of progress changed via the arrow keys.
+ * <p>
+ * By default, this will be a value that is derived from the max progress.
+ *
+ * @return The amount to increment or decrement when the user presses the
+ * arrow keys. This will be positive.
+ */
+ public int getKeyProgressIncrement() {
+ return mKeyProgressIncrement;
+ }
+
+ @Override
+ public synchronized void setMax(int max) {
+ super.setMax(max);
+
+ if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) {
+ // It will take the user too long to change this via keys, change it
+ // to something more reasonable
+ setKeyProgressIncrement(Math.max(1, Math.round((float) getMax() / 20)));
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mThumb || super.verifyDrawable(who);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mThumb != null) mThumb.jumpToCurrentState();
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ Drawable progressDrawable = getProgressDrawable();
+ if (progressDrawable != null) {
+ progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
+ }
+
+ if (mThumb != null && mThumb.isStateful()) {
+ int[] state = getDrawableState();
+ mThumb.setState(state);
+ }
+ }
+
+ @Override
+ void onProgressRefresh(float scale, boolean fromUser) {
+ super.onProgressRefresh(scale, fromUser);
+ Drawable thumb = mThumb;
+ if (thumb != null) {
+ setThumbPos(getWidth(), getHeight(), thumb, scale, Integer.MIN_VALUE);
+ /*
+ * Since we draw translated, the drawable's bounds that it signals
+ * for invalidation won't be the actual bounds we want invalidated,
+ * so just invalidate this whole view.
+ */
+ invalidate();
+ }
+ }
+
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ updateThumbPos(w, h);
+ }
+
+ private void updateThumbPos(int w, int h) {
+ Drawable d = getCurrentDrawable();
+ Drawable thumb = mThumb;
+ if (mIsVertical) {
+ int thumbWidth = thumb == null ? 0 : thumb.getIntrinsicWidth();
+ // The max width does not incorporate padding, whereas the width
+ // parameter does
+ int trackWidth = Math.min(mMaxWidth, w - mPaddingLeft - mPaddingRight);
+
+ int max = getMax();
+ float scale = max > 0 ? (float) getProgress() / (float) max : 0;
+
+ if (thumbWidth > trackWidth) {
+ int gapForCenteringTrack = (thumbWidth - trackWidth) / 2;
+ if (thumb != null) {
+ setThumbPos(w, h, thumb, scale, -gapForCenteringTrack);
+ }
+ if (d != null) {
+ // Canvas will be translated by the padding, so 0,0 is where we start drawing
+ d.setBounds(gapForCenteringTrack, 0,
+ w - mPaddingRight - gapForCenteringTrack - mPaddingLeft,
+ h - mPaddingBottom - mPaddingTop);
+ }
+ } else {
+ if (d != null) {
+ // Canvas will be translated by the padding, so 0,0 is where we start drawing
+ d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
+ - mPaddingTop);
+ }
+ int gap = (trackWidth - thumbWidth) / 2;
+ if (thumb != null) {
+ setThumbPos(w, h, thumb, scale, gap);
+ }
+ }
+ } else {
+ int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
+ // The max height does not incorporate padding, whereas the height
+ // parameter does
+ int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
+
+ int max = getMax();
+ float scale = max > 0 ? (float) getProgress() / (float) max : 0;
+
+ if (thumbHeight > trackHeight) {
+ if (thumb != null) {
+ setThumbPos(w, h, thumb, scale, 0);
+ }
+ int gapForCenteringTrack = (thumbHeight - trackHeight) / 2;
+ if (d != null) {
+ // Canvas will be translated by the padding, so 0,0 is where we start drawing
+ d.setBounds(0, gapForCenteringTrack,
+ w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack
+ - mPaddingTop);
+ }
+ } else {
+ if (d != null) {
+ // Canvas will be translated by the padding, so 0,0 is where we start drawing
+ d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
+ - mPaddingTop);
+ }
+ int gap = (trackHeight - thumbHeight) / 2;
+ if (thumb != null) {
+ setThumbPos(w, h, thumb, scale, gap);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and
+ */
+ private void setThumbPos(int w, int h, Drawable thumb, float scale, int gap) {
+ int available;
+ int thumbWidth = thumb.getIntrinsicWidth();
+ int thumbHeight = thumb.getIntrinsicHeight();
+ if (mIsVertical) {
+ available = h - mPaddingTop - mPaddingBottom - thumbHeight;
+ } else {
+ available = w - mPaddingLeft - mPaddingRight - thumbWidth;
+ }
+
+ // The extra space for the thumb to move on the track
+ available += mThumbOffset * 2;
+
+
+ if (mIsVertical) {
+ int thumbPos = (int) ((1.0f - scale) * available);
+ int leftBound, rightBound;
+ if (gap == Integer.MIN_VALUE) {
+ Rect oldBounds = thumb.getBounds();
+ leftBound = oldBounds.left;
+ rightBound = oldBounds.right;
+ } else {
+ leftBound = gap;
+ rightBound = gap + thumbWidth;
+ }
+
+ // Canvas will be translated, so 0,0 is where we start drawing
+ thumb.setBounds(leftBound, thumbPos, rightBound, thumbPos + thumbHeight);
+ } else {
+ int thumbPos = (int) (scale * available);
+ int topBound, bottomBound;
+ if (gap == Integer.MIN_VALUE) {
+ Rect oldBounds = thumb.getBounds();
+ topBound = oldBounds.top;
+ bottomBound = oldBounds.bottom;
+ } else {
+ topBound = gap;
+ bottomBound = gap + thumbHeight;
+ }
+
+ // Canvas will be translated, so 0,0 is where we start drawing
+ thumb.setBounds(thumbPos, topBound, thumbPos + thumbWidth, bottomBound);
+ }
+ }
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mThumb != null) {
+ canvas.save();
+ // Translate the padding. For the x/y, we need to allow the thumb to
+ // draw in its extra space
+ if (mIsVertical) {
+ canvas.translate(mPaddingLeft, mPaddingTop - mThumbOffset);
+ mThumb.draw(canvas);
+ canvas.restore();
+ } else {
+ canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
+ mThumb.draw(canvas);
+ canvas.restore();
+ }
+ }
+ }
+
+ @Override
+ protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Drawable d = getCurrentDrawable();
+
+ int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
+ int dw = 0;
+ int dh = 0;
+ if (d != null) {
+ dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
+ dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
+ dh = Math.max(thumbHeight, dh);
+ }
+ dw += mPaddingLeft + mPaddingRight;
+ dh += mPaddingTop + mPaddingBottom;
+
+ setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
+ resolveSizeAndState(dh, heightMeasureSpec, 0));
+
+ // TODO should probably make this an explicit attribute instead of implicitly
+ // setting it based on the size
+ if (getMeasuredHeight() > getMeasuredWidth()) {
+ mIsVertical = true;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!mIsUserSeekable || !isEnabled()) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (isInScrollingContainer()) {
+ mTouchDownX = event.getX();
+ mTouchDownY = event.getY();
+ } else {
+ setPressed(true);
+ if (mThumb != null) {
+ invalidate(mThumb.getBounds()); // This may be within the padding region
+ }
+ onStartTrackingTouch();
+ trackTouchEvent(event);
+ attemptClaimDrag();
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (mIsDragging) {
+ trackTouchEvent(event);
+ } else {
+ final float x = event.getX();
+ final float y = event.getX();
+ if (Math.abs(mIsVertical ?
+ (y - mTouchDownY) : (x - mTouchDownX)) > mScaledTouchSlop) {
+ setPressed(true);
+ if (mThumb != null) {
+ invalidate(mThumb.getBounds()); // This may be within the padding region
+ }
+ onStartTrackingTouch();
+ trackTouchEvent(event);
+ attemptClaimDrag();
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (mIsDragging) {
+ trackTouchEvent(event);
+ onStopTrackingTouch();
+ setPressed(false);
+ } else {
+ // Touch up when we never crossed the touch slop threshold should
+ // be interpreted as a tap-seek to that location.
+ onStartTrackingTouch();
+ trackTouchEvent(event);
+ onStopTrackingTouch();
+ }
+ // ProgressBar doesn't know to repaint the thumb drawable
+ // in its inactive state when the touch stops (because the
+ // value has not apparently changed)
+ invalidate();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsDragging) {
+ onStopTrackingTouch();
+ setPressed(false);
+ }
+ invalidate(); // see above explanation
+ break;
+ }
+ return true;
+ }
+
+ private void trackTouchEvent(MotionEvent event) {
+ float progress = 0;
+ if (mIsVertical) {
+ final int height = getHeight();
+ final int available = height - mPaddingTop - mPaddingBottom;
+ int y = (int)event.getY();
+ float scale;
+ if (y < mPaddingTop) {
+ scale = 1.0f;
+ } else if (y > height - mPaddingBottom) {
+ scale = 0.0f;
+ } else {
+ scale = 1.0f - (float)(y - mPaddingTop) / (float)available;
+ progress = mTouchProgressOffset;
+ }
+
+ final int max = getMax();
+ progress += scale * max;
+ } else {
+ final int width = getWidth();
+ final int available = width - mPaddingLeft - mPaddingRight;
+ int x = (int)event.getX();
+ float scale;
+ if (x < mPaddingLeft) {
+ scale = 0.0f;
+ } else if (x > width - mPaddingRight) {
+ scale = 1.0f;
+ } else {
+ scale = (float)(x - mPaddingLeft) / (float)available;
+ progress = mTouchProgressOffset;
+ }
+
+ final int max = getMax();
+ progress += scale * max;
+ }
+
+ setProgress((int) progress, true);
+ }
+
+ /**
+ * Tries to claim the user's drag motion, and requests disallowing any
+ * ancestors from stealing events in the drag.
+ */
+ private void attemptClaimDrag() {
+ if (mParent != null) {
+ mParent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ /**
+ * This is called when the user has started touching this widget.
+ */
+ void onStartTrackingTouch() {
+ mIsDragging = true;
+ }
+
+ /**
+ * This is called when the user either releases his touch or the touch is
+ * canceled.
+ */
+ void onStopTrackingTouch() {
+ mIsDragging = false;
+ }
+
+ /**
+ * Called when the user changes the seekbar's progress by using a key event.
+ */
+ void onKeyChange() {
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (isEnabled()) {
+ int progress = getProgress();
+ if ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !mIsVertical)
+ || (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && mIsVertical)) {
+ if (progress > 0) {
+ setProgress(progress - mKeyProgressIncrement, true);
+ onKeyChange();
+ return true;
+ }
+ } else if ((keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && !mIsVertical)
+ || (keyCode == KeyEvent.KEYCODE_DPAD_UP && mIsVertical)) {
+ if (progress < getMax()) {
+ setProgress(progress + mKeyProgressIncrement, true);
+ onKeyChange();
+ return true;
+ }
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+}
diff --git a/src/org/cyanogenmod/audiofx/seekbar/ProgressBar.java b/src/org/cyanogenmod/audiofx/seekbar/ProgressBar.java
new file mode 100644
index 0000000..92828ae
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/seekbar/ProgressBar.java
@@ -0,0 +1,1146 @@
+/*
+ * Copyright (C) 2006 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 org.cyanogenmod.audiofx.seekbar;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.Transformation;
+import android.widget.RemoteViews.RemoteView;
+
+
+/**
+ * <p>
+ * Visual indicator of progress in some operation. Displays a bar to the user
+ * representing how far the operation has progressed; the application can
+ * change the amount of progress (modifying the length of the bar) as it moves
+ * forward. There is also a secondary progress displayable on a progress bar
+ * which is useful for displaying intermediate progress, such as the buffer
+ * level during a streaming playback progress bar.
+ * </p>
+ *
+ * <p>
+ * A progress bar can also be made indeterminate. In indeterminate mode, the
+ * progress bar shows a cyclic animation without an indication of progress. This mode is used by
+ * applications when the length of the task is unknown. The indeterminate progress bar can be either
+ * a spinning wheel or a horizontal bar.
+ * </p>
+ *
+ * <p>The following code example shows how a progress bar can be used from
+ * a worker thread to update the user interface to notify the user of progress:
+ * </p>
+ *
+ * <pre>
+ * public class MyActivity extends Activity {
+ * private static final int PROGRESS = 0x1;
+ *
+ * private ProgressBar mProgress;
+ * private int mProgressStatus = 0;
+ *
+ * private Handler mHandler = new Handler();
+ *
+ * protected void onCreate(Bundle icicle) {
+ * super.onCreate(icicle);
+ *
+ * setContentView(R.layout.progressbar_activity);
+ *
+ * mProgress = (ProgressBar) findViewById(R.id.progress_bar);
+ *
+ * // Start lengthy operation in a background thread
+ * new Thread(new Runnable() {
+ * public void run() {
+ * while (mProgressStatus &lt; 100) {
+ * mProgressStatus = doWork();
+ *
+ * // Update the progress bar
+ * mHandler.post(new Runnable() {
+ * public void run() {
+ * mProgress.setProgress(mProgressStatus);
+ * }
+ * });
+ * }
+ * }
+ * }).start();
+ * }
+ * }</pre>
+ *
+ * <p>To add a progress bar to a layout file, you can use the {@code &lt;ProgressBar&gt;} element.
+ * By default, the progress bar is a spinning wheel (an indeterminate indicator). To change to a
+ * horizontal progress bar, apply the {@link android.R.style#Widget_ProgressBar_Horizontal
+ * Widget.ProgressBar.Horizontal} style, like so:</p>
+ *
+ * <pre>
+ * &lt;ProgressBar
+ * style="@android:style/Widget.ProgressBar.Horizontal"
+ * ... /&gt;</pre>
+ *
+ * <p>If you will use the progress bar to show real progress, you must use the horizontal bar. You
+ * can then increment the progress with {@link #incrementProgressBy incrementProgressBy()} or
+ * {@link #setProgress setProgress()}. By default, the progress bar is full when it reaches 100. If
+ * necessary, you can adjust the maximum value (the value for a full bar) using the {@link
+ * android.R.styleable#ProgressBar_max android:max} attribute. Other attributes available are listed
+ * below.</p>
+ *
+ * <p>Another common style to apply to the progress bar is {@link
+ * android.R.style#Widget_ProgressBar_Small Widget.ProgressBar.Small}, which shows a smaller
+ * version of the spinning wheel&mdash;useful when waiting for content to load.
+ * For example, you can insert this kind of progress bar into your default layout for
+ * a view that will be populated by some content fetched from the Internet&mdash;the spinning wheel
+ * appears immediately and when your application receives the content, it replaces the progress bar
+ * with the loaded content. For example:</p>
+ *
+ * <pre>
+ * &lt;LinearLayout
+ * android:orientation="horizontal"
+ * ... &gt;
+ * &lt;ProgressBar
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * style="@android:style/Widget.ProgressBar.Small"
+ * android:layout_marginRight="5dp" /&gt;
+ * &lt;TextView
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * android:text="@string/loading" /&gt;
+ * &lt;/LinearLayout&gt;</pre>
+ *
+ * <p>Other progress bar styles provided by the system include:</p>
+ * <ul>
+ * <li>{@link android.R.style#Widget_ProgressBar_Horizontal Widget.ProgressBar.Horizontal}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Small Widget.ProgressBar.Small}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Large Widget.ProgressBar.Large}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Inverse Widget.ProgressBar.Inverse}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Small_Inverse
+ * Widget.ProgressBar.Small.Inverse}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Large_Inverse
+ * Widget.ProgressBar.Large.Inverse}</li>
+ * </ul>
+ * <p>The "inverse" styles provide an inverse color scheme for the spinner, which may be necessary
+ * if your application uses a light colored theme (a white background).</p>
+ *
+ * <p><strong>XML attributes</b></strong>
+ * <p>
+ * See {@link android.R.styleable#ProgressBar ProgressBar Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ *
+ * @attr ref android.R.styleable#ProgressBar_animationResolution
+ * @attr ref android.R.styleable#ProgressBar_indeterminate
+ * @attr ref android.R.styleable#ProgressBar_indeterminateBehavior
+ * @attr ref android.R.styleable#ProgressBar_indeterminateDrawable
+ * @attr ref android.R.styleable#ProgressBar_indeterminateDuration
+ * @attr ref android.R.styleable#ProgressBar_indeterminateOnly
+ * @attr ref android.R.styleable#ProgressBar_interpolator
+ * @attr ref android.R.styleable#ProgressBar_max
+ * @attr ref android.R.styleable#ProgressBar_maxHeight
+ * @attr ref android.R.styleable#ProgressBar_maxWidth
+ * @attr ref android.R.styleable#ProgressBar_minHeight
+ * @attr ref android.R.styleable#ProgressBar_minWidth
+ * @attr ref android.R.styleable#ProgressBar_progress
+ * @attr ref android.R.styleable#ProgressBar_progressDrawable
+ * @attr ref android.R.styleable#ProgressBar_secondaryProgress
+ */
+@RemoteView
+public class ProgressBar extends View {
+ private static final int MAX_LEVEL = 10000;
+ private static final int ANIMATION_RESOLUTION = 200;
+ private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200;
+
+ int mMinWidth;
+ int mMaxWidth;
+ int mMinHeight;
+ int mMaxHeight;
+
+ private int mProgress;
+ private int mSecondaryProgress;
+ private int mMax;
+
+ private int mBehavior;
+ private int mDuration;
+ private boolean mIndeterminate;
+ private boolean mOnlyIndeterminate;
+ private Transformation mTransformation;
+ private AlphaAnimation mAnimation;
+ private Drawable mIndeterminateDrawable;
+ private Drawable mProgressDrawable;
+ private Drawable mCurrentDrawable;
+ Bitmap mSampleTile;
+ private boolean mNoInvalidate;
+ private Interpolator mInterpolator;
+ private RefreshProgressRunnable mRefreshProgressRunnable;
+ private long mUiThreadId;
+ private boolean mShouldStartAnimationDrawable;
+ private long mLastDrawTime;
+
+ private boolean mInDrawing;
+
+ private int mAnimationResolution;
+
+ private AccessibilityEventSender mAccessibilityEventSender;
+
+ /**
+ * Create a new progress bar with range 0...100 and initial progress of 0.
+ * @param context the application environment
+ */
+ public ProgressBar(Context context) {
+ this(context, null);
+ }
+
+ public ProgressBar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.progressBarStyle);
+ }
+
+ public ProgressBar(Context context, AttributeSet attrs, int defStyle) {
+ this(context, attrs, defStyle, 0);
+ }
+
+ /**
+ * @hide
+ */
+ public ProgressBar(Context context, AttributeSet attrs, int defStyle, int styleRes) {
+ super(context, attrs, defStyle);
+ mUiThreadId = Thread.currentThread().getId();
+ initProgressBar();
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, R.styleable.ProgressBar, defStyle, styleRes);
+
+ mNoInvalidate = true;
+
+ Drawable drawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable);
+ if (drawable != null) {
+ drawable = tileify(drawable, false);
+ // Calling this method can set mMaxHeight, make sure the corresponding
+ // XML attribute for mMaxHeight is read after calling this method
+ setProgressDrawable(drawable);
+ }
+
+
+ mDuration = a.getInt(R.styleable.ProgressBar_indeterminateDuration, mDuration);
+
+ mMinWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_minWidth, mMinWidth);
+ mMaxWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_maxWidth, mMaxWidth);
+ mMinHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_minHeight, mMinHeight);
+ mMaxHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_maxHeight, mMaxHeight);
+
+ mBehavior = a.getInt(R.styleable.ProgressBar_indeterminateBehavior, mBehavior);
+
+ final int resID = a.getResourceId(
+ com.android.internal.R.styleable.ProgressBar_interpolator,
+ android.R.anim.linear_interpolator); // default to linear interpolator
+ if (resID > 0) {
+ setInterpolator(context, resID);
+ }
+
+ setMax(a.getInt(R.styleable.ProgressBar_max, mMax));
+
+ setProgress(a.getInt(R.styleable.ProgressBar_progress, mProgress));
+
+ setSecondaryProgress(
+ a.getInt(R.styleable.ProgressBar_secondaryProgress, mSecondaryProgress));
+
+ drawable = a.getDrawable(R.styleable.ProgressBar_indeterminateDrawable);
+ if (drawable != null) {
+ drawable = tileifyIndeterminate(drawable);
+ setIndeterminateDrawable(drawable);
+ }
+
+ mOnlyIndeterminate = a.getBoolean(
+ R.styleable.ProgressBar_indeterminateOnly, mOnlyIndeterminate);
+
+ mNoInvalidate = false;
+
+ setIndeterminate(mOnlyIndeterminate || a.getBoolean(
+ R.styleable.ProgressBar_indeterminate, mIndeterminate));
+
+ mAnimationResolution = a.getInteger(R.styleable.ProgressBar_animationResolution,
+ ANIMATION_RESOLUTION);
+
+ a.recycle();
+ }
+
+ /**
+ * Converts a drawable to a tiled version of itself. It will recursively
+ * traverse layer and state list drawables.
+ */
+ private Drawable tileify(Drawable drawable, boolean clip) {
+
+ if (drawable instanceof LayerDrawable) {
+ LayerDrawable background = (LayerDrawable) drawable;
+ final int N = background.getNumberOfLayers();
+ Drawable[] outDrawables = new Drawable[N];
+
+ for (int i = 0; i < N; i++) {
+ int id = background.getId(i);
+ outDrawables[i] = tileify(background.getDrawable(i),
+ (id == R.id.progress || id == R.id.secondaryProgress));
+ }
+
+ LayerDrawable newBg = new LayerDrawable(outDrawables);
+
+ for (int i = 0; i < N; i++) {
+ newBg.setId(i, background.getId(i));
+ }
+
+ return newBg;
+
+ } else if (drawable instanceof StateListDrawable) {
+ StateListDrawable in = (StateListDrawable) drawable;
+ StateListDrawable out = new StateListDrawable();
+ int numStates = in.getStateCount();
+ for (int i = 0; i < numStates; i++) {
+ out.addState(in.getStateSet(i), tileify(in.getStateDrawable(i), clip));
+ }
+ return out;
+
+ } else if (drawable instanceof BitmapDrawable) {
+ final Bitmap tileBitmap = ((BitmapDrawable) drawable).getBitmap();
+ if (mSampleTile == null) {
+ mSampleTile = tileBitmap;
+ }
+
+ final ShapeDrawable shapeDrawable = new ShapeDrawable(getDrawableShape());
+
+ final BitmapShader bitmapShader = new BitmapShader(tileBitmap,
+ Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
+ shapeDrawable.getPaint().setShader(bitmapShader);
+
+ return (clip) ? new ClipDrawable(shapeDrawable, Gravity.LEFT,
+ ClipDrawable.HORIZONTAL) : shapeDrawable;
+ }
+
+ return drawable;
+ }
+
+ Shape getDrawableShape() {
+ final float[] roundedCorners = new float[] { 5, 5, 5, 5, 5, 5, 5, 5 };
+ return new RoundRectShape(roundedCorners, null, null);
+ }
+
+ /**
+ * Convert a AnimationDrawable for use as a barberpole animation.
+ * Each frame of the animation is wrapped in a ClipDrawable and
+ * given a tiling BitmapShader.
+ */
+ private Drawable tileifyIndeterminate(Drawable drawable) {
+ if (drawable instanceof AnimationDrawable) {
+ AnimationDrawable background = (AnimationDrawable) drawable;
+ final int N = background.getNumberOfFrames();
+ AnimationDrawable newBg = new AnimationDrawable();
+ newBg.setOneShot(background.isOneShot());
+
+ for (int i = 0; i < N; i++) {
+ Drawable frame = tileify(background.getFrame(i), true);
+ frame.setLevel(10000);
+ newBg.addFrame(frame, background.getDuration(i));
+ }
+ newBg.setLevel(10000);
+ drawable = newBg;
+ }
+ return drawable;
+ }
+
+ /**
+ * <p>
+ * Initialize the progress bar's default values:
+ * </p>
+ * <ul>
+ * <li>progress = 0</li>
+ * <li>max = 100</li>
+ * <li>animation duration = 4000 ms</li>
+ * <li>indeterminate = false</li>
+ * <li>behavior = repeat</li>
+ * </ul>
+ */
+ private void initProgressBar() {
+ mMax = 100;
+ mProgress = 0;
+ mSecondaryProgress = 0;
+ mIndeterminate = false;
+ mOnlyIndeterminate = false;
+ mDuration = 4000;
+ mBehavior = AlphaAnimation.RESTART;
+ mMinWidth = 24;
+ mMaxWidth = 48;
+ mMinHeight = 24;
+ mMaxHeight = 48;
+ }
+
+ /**
+ * <p>Indicate whether this progress bar is in indeterminate mode.</p>
+ *
+ * @return true if the progress bar is in indeterminate mode
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ public synchronized boolean isIndeterminate() {
+ return mIndeterminate;
+ }
+
+ /**
+ * <p>Change the indeterminate mode for this progress bar. In indeterminate
+ * mode, the progress is ignored and the progress bar shows an infinite
+ * animation instead.</p>
+ *
+ * If this progress bar's style only supports indeterminate mode (such as the circular
+ * progress bars), then this will be ignored.
+ *
+ * @param indeterminate true to enable the indeterminate mode
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setIndeterminate(boolean indeterminate) {
+ if ((!mOnlyIndeterminate || !mIndeterminate) && indeterminate != mIndeterminate) {
+ mIndeterminate = indeterminate;
+
+ if (indeterminate) {
+ // swap between indeterminate and regular backgrounds
+ mCurrentDrawable = mIndeterminateDrawable;
+ startAnimation();
+ } else {
+ mCurrentDrawable = mProgressDrawable;
+ stopAnimation();
+ }
+ }
+ }
+
+ /**
+ * <p>Get the drawable used to draw the progress bar in
+ * indeterminate mode.</p>
+ *
+ * @return a {@link android.graphics.drawable.Drawable} instance
+ *
+ * @see #setIndeterminateDrawable(android.graphics.drawable.Drawable)
+ * @see #setIndeterminate(boolean)
+ */
+ public Drawable getIndeterminateDrawable() {
+ return mIndeterminateDrawable;
+ }
+
+ /**
+ * <p>Define the drawable used to draw the progress bar in
+ * indeterminate mode.</p>
+ *
+ * @param d the new drawable
+ *
+ * @see #getIndeterminateDrawable()
+ * @see #setIndeterminate(boolean)
+ */
+ public void setIndeterminateDrawable(Drawable d) {
+ if (d != null) {
+ d.setCallback(this);
+ }
+ mIndeterminateDrawable = d;
+ if (mIndeterminate) {
+ mCurrentDrawable = d;
+ postInvalidate();
+ }
+ }
+
+ /**
+ * <p>Get the drawable used to draw the progress bar in
+ * progress mode.</p>
+ *
+ * @return a {@link android.graphics.drawable.Drawable} instance
+ *
+ * @see #setProgressDrawable(android.graphics.drawable.Drawable)
+ * @see #setIndeterminate(boolean)
+ */
+ public Drawable getProgressDrawable() {
+ return mProgressDrawable;
+ }
+
+ /**
+ * <p>Define the drawable used to draw the progress bar in
+ * progress mode.</p>
+ *
+ * @param d the new drawable
+ *
+ * @see #getProgressDrawable()
+ * @see #setIndeterminate(boolean)
+ */
+ public void setProgressDrawable(Drawable d) {
+ boolean needUpdate;
+ if (mProgressDrawable != null && d != mProgressDrawable) {
+ mProgressDrawable.setCallback(null);
+ needUpdate = true;
+ } else {
+ needUpdate = false;
+ }
+
+ if (d != null) {
+ d.setCallback(this);
+
+ // Make sure the ProgressBar is always tall enough
+ int drawableHeight = d.getMinimumHeight();
+ if (mMaxHeight < drawableHeight) {
+ mMaxHeight = drawableHeight;
+ requestLayout();
+ }
+ }
+ mProgressDrawable = d;
+ if (!mIndeterminate) {
+ mCurrentDrawable = d;
+ postInvalidate();
+ }
+
+ if (needUpdate) {
+ updateDrawableBounds(getWidth(), getHeight());
+ updateDrawableState();
+ doRefreshProgress(R.id.progress, mProgress, false, false);
+ doRefreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, false);
+ }
+ }
+
+ /**
+ * @return The drawable currently used to draw the progress bar
+ */
+ Drawable getCurrentDrawable() {
+ return mCurrentDrawable;
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mProgressDrawable || who == mIndeterminateDrawable
+ || super.verifyDrawable(who);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mProgressDrawable != null) mProgressDrawable.jumpToCurrentState();
+ if (mIndeterminateDrawable != null) mIndeterminateDrawable.jumpToCurrentState();
+ }
+
+ @Override
+ public void postInvalidate() {
+ if (!mNoInvalidate) {
+ super.postInvalidate();
+ }
+ }
+
+ private class RefreshProgressRunnable implements Runnable {
+
+ private int mId;
+ private int mProgress;
+ private boolean mFromUser;
+
+ RefreshProgressRunnable(int id, int progress, boolean fromUser) {
+ mId = id;
+ mProgress = progress;
+ mFromUser = fromUser;
+ }
+
+ public void run() {
+ doRefreshProgress(mId, mProgress, mFromUser, true);
+ // Put ourselves back in the cache when we are done
+ mRefreshProgressRunnable = this;
+ }
+
+ public void setup(int id, int progress, boolean fromUser) {
+ mId = id;
+ mProgress = progress;
+ mFromUser = fromUser;
+ }
+
+ }
+
+ private synchronized void doRefreshProgress(int id, int progress, boolean fromUser,
+ boolean callBackToApp) {
+ float scale = mMax > 0 ? (float) progress / (float) mMax : 0;
+ final Drawable d = mCurrentDrawable;
+ if (d != null) {
+ Drawable progressDrawable = null;
+
+ if (d instanceof LayerDrawable) {
+ progressDrawable = ((LayerDrawable) d).findDrawableByLayerId(id);
+ }
+
+ final int level = (int) (scale * MAX_LEVEL);
+ (progressDrawable != null ? progressDrawable : d).setLevel(level);
+ } else {
+ invalidate();
+ }
+
+ if (callBackToApp && id == R.id.progress) {
+ onProgressRefresh(scale, fromUser);
+ }
+ }
+
+ void onProgressRefresh(float scale, boolean fromUser) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ scheduleAccessibilityEventSender();
+ }
+ }
+
+ private synchronized void refreshProgress(int id, int progress, boolean fromUser) {
+ if (mUiThreadId == Thread.currentThread().getId()) {
+ doRefreshProgress(id, progress, fromUser, true);
+ } else {
+ RefreshProgressRunnable r;
+ if (mRefreshProgressRunnable != null) {
+ // Use cached RefreshProgressRunnable if available
+ r = mRefreshProgressRunnable;
+ // Uncache it
+ mRefreshProgressRunnable = null;
+ r.setup(id, progress, fromUser);
+ } else {
+ // Make a new one
+ r = new RefreshProgressRunnable(id, progress, fromUser);
+ }
+ post(r);
+ }
+ }
+
+ /**
+ * <p>Set the current progress to the specified value. Does not do anything
+ * if the progress bar is in indeterminate mode.</p>
+ *
+ * @param progress the new progress, between 0 and {@link #getMax()}
+ *
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #getProgress()
+ * @see #incrementProgressBy(int)
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setProgress(int progress) {
+ setProgress(progress, false);
+ }
+
+ @android.view.RemotableViewMethod
+ synchronized void setProgress(int progress, boolean fromUser) {
+ if (mIndeterminate) {
+ return;
+ }
+
+ if (progress < 0) {
+ progress = 0;
+ }
+
+ if (progress > mMax) {
+ progress = mMax;
+ }
+
+ if (progress != mProgress) {
+ mProgress = progress;
+ refreshProgress(R.id.progress, mProgress, fromUser);
+ }
+ }
+
+ /**
+ * <p>
+ * Set the current secondary progress to the specified value. Does not do
+ * anything if the progress bar is in indeterminate mode.
+ * </p>
+ *
+ * @param secondaryProgress the new secondary progress, between 0 and {@link #getMax()}
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #getSecondaryProgress()
+ * @see #incrementSecondaryProgressBy(int)
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setSecondaryProgress(int secondaryProgress) {
+ if (mIndeterminate) {
+ return;
+ }
+
+ if (secondaryProgress < 0) {
+ secondaryProgress = 0;
+ }
+
+ if (secondaryProgress > mMax) {
+ secondaryProgress = mMax;
+ }
+
+ if (secondaryProgress != mSecondaryProgress) {
+ mSecondaryProgress = secondaryProgress;
+ refreshProgress(R.id.secondaryProgress, mSecondaryProgress, false);
+ }
+ }
+
+ /**
+ * <p>Get the progress bar's current level of progress. Return 0 when the
+ * progress bar is in indeterminate mode.</p>
+ *
+ * @return the current progress, between 0 and {@link #getMax()}
+ *
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #setProgress(int)
+ * @see #setMax(int)
+ * @see #getMax()
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ public synchronized int getProgress() {
+ return mIndeterminate ? 0 : mProgress;
+ }
+
+ /**
+ * <p>Get the progress bar's current level of secondary progress. Return 0 when the
+ * progress bar is in indeterminate mode.</p>
+ *
+ * @return the current secondary progress, between 0 and {@link #getMax()}
+ *
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #setSecondaryProgress(int)
+ * @see #setMax(int)
+ * @see #getMax()
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ public synchronized int getSecondaryProgress() {
+ return mIndeterminate ? 0 : mSecondaryProgress;
+ }
+
+ /**
+ * <p>Return the upper limit of this progress bar's range.</p>
+ *
+ * @return a positive integer
+ *
+ * @see #setMax(int)
+ * @see #getProgress()
+ * @see #getSecondaryProgress()
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ public synchronized int getMax() {
+ return mMax;
+ }
+
+ /**
+ * <p>Set the range of the progress bar to 0...<tt>max</tt>.</p>
+ *
+ * @param max the upper range of this progress bar
+ *
+ * @see #getMax()
+ * @see #setProgress(int)
+ * @see #setSecondaryProgress(int)
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setMax(int max) {
+ if (max < 0) {
+ max = 0;
+ }
+ if (max != mMax) {
+ mMax = max;
+ postInvalidate();
+
+ if (mProgress > max) {
+ mProgress = max;
+ }
+ refreshProgress(R.id.progress, mProgress, false);
+ }
+ }
+
+ /**
+ * <p>Increase the progress bar's progress by the specified amount.</p>
+ *
+ * @param diff the amount by which the progress must be increased
+ *
+ * @see #setProgress(int)
+ */
+ public synchronized final void incrementProgressBy(int diff) {
+ setProgress(mProgress + diff);
+ }
+
+ /**
+ * <p>Increase the progress bar's secondary progress by the specified amount.</p>
+ *
+ * @param diff the amount by which the secondary progress must be increased
+ *
+ * @see #setSecondaryProgress(int)
+ */
+ public synchronized final void incrementSecondaryProgressBy(int diff) {
+ setSecondaryProgress(mSecondaryProgress + diff);
+ }
+
+ /**
+ * <p>Start the indeterminate progress animation.</p>
+ */
+ void startAnimation() {
+ if (getVisibility() != VISIBLE) {
+ return;
+ }
+
+ if (mIndeterminateDrawable instanceof Animatable) {
+ mShouldStartAnimationDrawable = true;
+ mAnimation = null;
+ } else {
+ if (mInterpolator == null) {
+ mInterpolator = new LinearInterpolator();
+ }
+
+ mTransformation = new Transformation();
+ mAnimation = new AlphaAnimation(0.0f, 1.0f);
+ mAnimation.setRepeatMode(mBehavior);
+ mAnimation.setRepeatCount(Animation.INFINITE);
+ mAnimation.setDuration(mDuration);
+ mAnimation.setInterpolator(mInterpolator);
+ mAnimation.setStartTime(Animation.START_ON_FIRST_FRAME);
+ }
+ postInvalidate();
+ }
+
+ /**
+ * <p>Stop the indeterminate progress animation.</p>
+ */
+ void stopAnimation() {
+ mAnimation = null;
+ mTransformation = null;
+ if (mIndeterminateDrawable instanceof Animatable) {
+ ((Animatable) mIndeterminateDrawable).stop();
+ mShouldStartAnimationDrawable = false;
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Sets the acceleration curve for the indeterminate animation.
+ * The interpolator is loaded as a resource from the specified context.
+ *
+ * @param context The application environment
+ * @param resID The resource identifier of the interpolator to load
+ */
+ public void setInterpolator(Context context, int resID) {
+ setInterpolator(AnimationUtils.loadInterpolator(context, resID));
+ }
+
+ /**
+ * Sets the acceleration curve for the indeterminate animation.
+ * Defaults to a linear interpolation.
+ *
+ * @param interpolator The interpolator which defines the acceleration curve
+ */
+ public void setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ /**
+ * Gets the acceleration curve type for the indeterminate animation.
+ *
+ * @return the {@link Interpolator} associated to this animation
+ */
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ @Override
+ @RemotableViewMethod
+ public void setVisibility(int v) {
+ if (getVisibility() != v) {
+ super.setVisibility(v);
+
+ if (mIndeterminate) {
+ // let's be nice with the UI thread
+ if (v == GONE || v == INVISIBLE) {
+ stopAnimation();
+ } else {
+ startAnimation();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+
+ if (mIndeterminate) {
+ // let's be nice with the UI thread
+ if (visibility == GONE || visibility == INVISIBLE) {
+ stopAnimation();
+ } else {
+ startAnimation();
+ }
+ }
+ }
+
+ @Override
+ public void invalidateDrawable(Drawable dr) {
+ if (!mInDrawing) {
+ if (verifyDrawable(dr)) {
+ final Rect dirty = dr.getBounds();
+ final int scrollX = mScrollX + mPaddingLeft;
+ final int scrollY = mScrollY + mPaddingTop;
+
+ invalidate(dirty.left + scrollX, dirty.top + scrollY,
+ dirty.right + scrollX, dirty.bottom + scrollY);
+ } else {
+ super.invalidateDrawable(dr);
+ }
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ updateDrawableBounds(w, h);
+ }
+
+ private void updateDrawableBounds(int w, int h) {
+ // onDraw will translate the canvas so we draw starting at 0,0
+ int right = w - mPaddingRight - mPaddingLeft;
+ int bottom = h - mPaddingBottom - mPaddingTop;
+ int top = 0;
+ int left = 0;
+
+ if (mIndeterminateDrawable != null) {
+ // Aspect ratio logic does not apply to AnimationDrawables
+ if (mOnlyIndeterminate && !(mIndeterminateDrawable instanceof AnimationDrawable)) {
+ // Maintain aspect ratio. Certain kinds of animated drawables
+ // get very confused otherwise.
+ final int intrinsicWidth = mIndeterminateDrawable.getIntrinsicWidth();
+ final int intrinsicHeight = mIndeterminateDrawable.getIntrinsicHeight();
+ final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight;
+ final float boundAspect = (float) w / h;
+ if (intrinsicAspect != boundAspect) {
+ if (boundAspect > intrinsicAspect) {
+ // New width is larger. Make it smaller to match height.
+ final int width = (int) (h * intrinsicAspect);
+ left = (w - width) / 2;
+ right = left + width;
+ } else {
+ // New height is larger. Make it smaller to match width.
+ final int height = (int) (w * (1 / intrinsicAspect));
+ top = (h - height) / 2;
+ bottom = top + height;
+ }
+ }
+ }
+ mIndeterminateDrawable.setBounds(left, top, right, bottom);
+ }
+
+ if (mProgressDrawable != null) {
+ mProgressDrawable.setBounds(0, 0, right, bottom);
+ }
+ }
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ Drawable d = mCurrentDrawable;
+ if (d != null) {
+ // Translate canvas so a indeterminate circular progress bar with padding
+ // rotates properly in its animation
+ canvas.save();
+ canvas.translate(mPaddingLeft, mPaddingTop);
+ long time = getDrawingTime();
+ if (mAnimation != null) {
+ mAnimation.getTransformation(time, mTransformation);
+ float scale = mTransformation.getAlpha();
+ try {
+ mInDrawing = true;
+ d.setLevel((int) (scale * MAX_LEVEL));
+ } finally {
+ mInDrawing = false;
+ }
+ if (SystemClock.uptimeMillis() - mLastDrawTime >= mAnimationResolution) {
+ mLastDrawTime = SystemClock.uptimeMillis();
+ postInvalidateDelayed(mAnimationResolution);
+ }
+ }
+ d.draw(canvas);
+ canvas.restore();
+ if (mShouldStartAnimationDrawable && d instanceof Animatable) {
+ ((Animatable) d).start();
+ mShouldStartAnimationDrawable = false;
+ }
+ }
+ }
+
+ @Override
+ protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Drawable d = mCurrentDrawable;
+
+ int dw = 0;
+ int dh = 0;
+ if (d != null) {
+ dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
+ dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
+ }
+ updateDrawableState();
+ dw += mPaddingLeft + mPaddingRight;
+ dh += mPaddingTop + mPaddingBottom;
+
+ setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
+ resolveSizeAndState(dh, heightMeasureSpec, 0));
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ updateDrawableState();
+ }
+
+ private void updateDrawableState() {
+ int[] state = getDrawableState();
+
+ if (mProgressDrawable != null && mProgressDrawable.isStateful()) {
+ mProgressDrawable.setState(state);
+ }
+
+ if (mIndeterminateDrawable != null && mIndeterminateDrawable.isStateful()) {
+ mIndeterminateDrawable.setState(state);
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ int progress;
+ int secondaryProgress;
+
+ /**
+ * Constructor called from {@link ProgressBar#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ progress = in.readInt();
+ secondaryProgress = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(progress);
+ out.writeInt(secondaryProgress);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ // Force our ancestor class to save its state
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+
+ ss.progress = mProgress;
+ ss.secondaryProgress = mSecondaryProgress;
+
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ setProgress(ss.progress);
+ setSecondaryProgress(ss.secondaryProgress);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mIndeterminate) {
+ startAnimation();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (mIndeterminate) {
+ stopAnimation();
+ }
+ if(mRefreshProgressRunnable != null) {
+ removeCallbacks(mRefreshProgressRunnable);
+ }
+ if (mAccessibilityEventSender != null) {
+ removeCallbacks(mAccessibilityEventSender);
+ }
+ // This should come after stopAnimation(), otherwise an invalidate message remains in the
+ // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setItemCount(mMax);
+ event.setCurrentItemIndex(mProgress);
+ }
+
+ /**
+ * Schedule a command for sending an accessibility event.
+ * </br>
+ * Note: A command is used to ensure that accessibility events
+ * are sent at most one in a given time frame to save
+ * system resources while the progress changes quickly.
+ */
+ private void scheduleAccessibilityEventSender() {
+ if (mAccessibilityEventSender == null) {
+ mAccessibilityEventSender = new AccessibilityEventSender();
+ } else {
+ removeCallbacks(mAccessibilityEventSender);
+ }
+ postDelayed(mAccessibilityEventSender, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
+ }
+
+ /**
+ * Command for sending an accessibility event.
+ */
+ private class AccessibilityEventSender implements Runnable {
+ public void run() {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/seekbar/SeekBar.java b/src/org/cyanogenmod/audiofx/seekbar/SeekBar.java
new file mode 100644
index 0000000..a137a40
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/seekbar/SeekBar.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2006 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 org.cyanogenmod.audiofx.seekbar;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+
+
+/**
+ * A SeekBar is an extension of ProgressBar that adds a draggable thumb. The user can touch
+ * the thumb and drag left or right to set the current progress level or use the arrow keys.
+ * Placing focusable widgets to the left or right of a SeekBar is discouraged.
+ * <p>
+ * Clients of the SeekBar can attach a {@link SeekBar.OnSeekBarChangeListener} to
+ * be notified of the user's actions.
+ *
+ * @attr ref android.R.styleable#SeekBar_thumb
+ */
+public class SeekBar extends AbsSeekBar {
+
+ /**
+ * A callback that notifies clients when the progress level has been
+ * changed. This includes changes that were initiated by the user through a
+ * touch gesture or arrow key/trackball as well as changes that were initiated
+ * programmatically.
+ */
+ public interface OnSeekBarChangeListener {
+
+ /**
+ * Notification that the progress level has changed. Clients can use the fromUser parameter
+ * to distinguish user-initiated changes from those that occurred programmatically.
+ *
+ * @param seekBar The SeekBar whose progress has changed
+ * @param progress The current progress level. This will be in the range 0..max where max
+ * was set by {@link ProgressBar#setMax(int)}. (The default value for max is 100.)
+ * @param fromUser True if the progress change was initiated by the user.
+ */
+ void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser);
+
+ /**
+ * Notification that the user has started a touch gesture. Clients may want to use this
+ * to disable advancing the seekbar.
+ * @param seekBar The SeekBar in which the touch gesture began
+ */
+ void onStartTrackingTouch(SeekBar seekBar);
+
+ /**
+ * Notification that the user has finished a touch gesture. Clients may want to use this
+ * to re-enable advancing the seekbar.
+ * @param seekBar The SeekBar in which the touch gesture began
+ */
+ void onStopTrackingTouch(SeekBar seekBar);
+ }
+
+ private OnSeekBarChangeListener mOnSeekBarChangeListener;
+
+ public SeekBar(Context context) {
+ this(context, null);
+ }
+
+ public SeekBar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.seekBarStyle);
+ }
+
+ public SeekBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ void onProgressRefresh(float scale, boolean fromUser) {
+ super.onProgressRefresh(scale, fromUser);
+
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onProgressChanged(this, getProgress(), fromUser);
+ }
+ }
+
+ /**
+ * Sets a listener to receive notifications of changes to the SeekBar's progress level. Also
+ * provides notifications of when the user starts and stops a touch gesture within the SeekBar.
+ *
+ * @param l The seek bar notification listener
+ *
+ * @see SeekBar.OnSeekBarChangeListener
+ */
+ public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) {
+ mOnSeekBarChangeListener = l;
+ }
+
+ @Override
+ void onStartTrackingTouch() {
+ super.onStartTrackingTouch();
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onStartTrackingTouch(this);
+ }
+ }
+
+ @Override
+ void onStopTrackingTouch() {
+ super.onStopTrackingTouch();
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onStopTrackingTouch(this);
+ }
+ }
+
+}
diff --git a/src/org/cyanogenmod/audiofx/widget/Biquad.java b/src/org/cyanogenmod/audiofx/widget/Biquad.java
new file mode 100644
index 0000000..f90153d
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/widget/Biquad.java
@@ -0,0 +1,31 @@
+package org.cyanogenmod.audiofx.widget;
+
+/**
+ * Evaluate transfer functions of biquad filters in direct form 1.
+ *
+ * @author alankila
+ */
+class Biquad {
+ private Complex mB0, mB1, mB2, mA0, mA1, mA2;
+
+ protected void setHighShelf(double centerFrequency, double samplingFrequency,
+ double dbGain, double slope) {
+ double w0 = 2 * Math.PI * centerFrequency / samplingFrequency;
+ double a = Math.pow(10, dbGain/40);
+ double alpha = Math.sin(w0) / 2 * Math.sqrt((a + 1 / a) * (1 / slope - 1) + 2);
+
+ mB0 = new Complex(a*((a+1) + (a-1) *Math.cos(w0) + 2*Math.sqrt(a)*alpha), 0);
+ mB1 = new Complex(-2*a*((a-1) + (a+1)*Math.cos(w0)), 0);
+ mB2 = new Complex(a*((a+1) + (a-1) *Math.cos(w0) - 2*Math.sqrt(a)*alpha), 0);
+ mA0 = new Complex((a+1) - (a-1) *Math.cos(w0) + 2*Math.sqrt(a)*alpha, 0);
+ mA1 = new Complex(2*((a-1) - (a+1) *Math.cos(w0)), 0);
+ mA2 = new Complex((a+1) - (a-1) *Math.cos(w0) - 2*Math.sqrt(a)*alpha, 0);
+ }
+
+ protected Complex evaluateTransfer(Complex z) {
+ Complex zSquared = z.mul(z);
+ Complex nom = mB0.add(mB1.div(z)).add(mB2.div(zSquared));
+ Complex den = mA0.add(mA1.div(z)).add(mA2.div(zSquared));
+ return nom.div(den);
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/widget/Complex.java b/src/org/cyanogenmod/audiofx/widget/Complex.java
new file mode 100644
index 0000000..b4691a3
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/widget/Complex.java
@@ -0,0 +1,94 @@
+package org.cyanogenmod.audiofx.widget;
+
+/**
+ * Java support for complex numbers.
+ *
+ * @author alankila
+ */
+class Complex {
+ private final double mReal, mIm;
+
+ protected Complex(double real, double im) {
+ mReal = real;
+ mIm = im;
+ }
+
+ /**
+ * Length of complex number
+ *
+ * @return length
+ */
+ protected double rho() {
+ return Math.sqrt(mReal * mReal + mIm * mIm);
+ }
+
+ /**
+ * Argument of complex number
+ *
+ * @return angle in radians
+ */
+ protected double theta() {
+ return Math.atan2(mIm, mReal);
+ }
+
+ /**
+ * Complex conjugate
+ *
+ * @return conjugate
+ */
+ protected Complex con() {
+ return new Complex(mReal, -mIm);
+ }
+
+ /**
+ * Complex addition
+ *
+ * @param other
+ * @return sum
+ */
+ protected Complex add(Complex other) {
+ return new Complex(mReal + other.mReal, mIm + other.mIm);
+ }
+
+ /**
+ * Complex multipply
+ *
+ * @param other
+ * @return multiplication result
+ */
+ protected Complex mul(Complex other) {
+ return new Complex(mReal * other.mReal - mIm * other.mIm,
+ mReal * other.mIm + mIm * other.mReal);
+ }
+
+ /**
+ * Complex multiply with real value
+ *
+ * @param a
+ * @return multiplication result
+ */
+ protected Complex mul(double a) {
+ return new Complex(mReal * a, mIm * a);
+ }
+
+ /**
+ * Complex division
+ *
+ * @param other
+ * @return division result
+ */
+ protected Complex div(Complex other) {
+ double lengthSquared = other.mReal * other.mReal + other.mIm * other.mIm;
+ return mul(other.con()).div(lengthSquared);
+ }
+
+ /**
+ * Complex division with real value
+ *
+ * @param a
+ * @return division result
+ */
+ protected Complex div(double a) {
+ return new Complex(mReal / a, mIm / a);
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java b/src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java
new file mode 100644
index 0000000..c8b39e3
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.
+ *
+ * - Original code by Antti S. Lankila for DSPManager
+ * - Modified extensively by cyanogen for multi-band support
+ */
+
+package org.cyanogenmod.audiofx.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Paint.Cap;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.Shader;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceView;
+import android.view.View;
+
+import org.cyanogenmod.audiofx.R;
+import org.cyanogenmod.audiofx.widget.EqualizerSurface.BandUpdatedListener;
+
+import java.util.Arrays;
+
+public class EqualizerSurface extends SurfaceView {
+
+ private static int SAMPLING_RATE = 44100;
+
+ private int mWidth;
+ private int mHeight;
+
+ private float mMinFreq = 10;
+ private float mMaxFreq = 21000;
+
+ private float mMinDB = -15;
+ private float mMaxDB = 15;
+
+ private int mNumBands = 5;
+
+ private boolean mReadOnly = false;
+
+ private float[] mLevels = new float[mNumBands];
+ private float[] mCenterFreqs = new float[mNumBands];
+ private final Paint mWhite, mGridLines, mControlBarText, mControlBar;
+ private final Paint mFrequencyResponseBg;
+ private final Paint mFrequencyResponseHighlight, mFrequencyResponseHighlight2;
+
+ private BandUpdatedListener mBandUpdatedListener;
+
+ public EqualizerSurface(Context context, AttributeSet attributeSet) {
+ super(context, attributeSet);
+ setWillNotDraw(false);
+
+ mWhite = new Paint();
+ mWhite.setColor(getResources().getColor(R.color.white));
+ mWhite.setStyle(Style.STROKE);
+ mWhite.setTextSize(context.getResources().getDimensionPixelSize(R.dimen.eq_label_text_size));
+ mWhite.setTypeface(Typeface.DEFAULT_BOLD);
+ mWhite.setAntiAlias(true);
+
+ mGridLines = new Paint();
+ mGridLines.setColor(getResources().getColor(R.color.grid_lines));
+ mGridLines.setStyle(Style.STROKE);
+
+ mControlBarText = new Paint(mWhite);
+ mControlBarText.setTextAlign(Paint.Align.CENTER);
+ mControlBarText.setShadowLayer(2, 0, 0, getResources().getColor(R.color.cb));
+
+ mControlBar = new Paint();
+ mControlBar.setStyle(Style.STROKE);
+ mControlBar.setColor(getResources().getColor(R.color.cb));
+ mControlBar.setAntiAlias(true);
+ mControlBar.setStrokeCap(Cap.ROUND);
+ mControlBar.setShadowLayer(2, 0, 0, getResources().getColor(R.color.black));
+
+ mFrequencyResponseBg = new Paint();
+ mFrequencyResponseBg.setStyle(Style.FILL);
+ mFrequencyResponseBg.setAntiAlias(true);
+
+ mFrequencyResponseHighlight = new Paint();
+ mFrequencyResponseHighlight.setStyle(Style.STROKE);
+ mFrequencyResponseHighlight.setStrokeWidth(6);
+ mFrequencyResponseHighlight.setColor(getResources().getColor(R.color.freq_hl));
+ mFrequencyResponseHighlight.setAntiAlias(true);
+
+ mFrequencyResponseHighlight2 = new Paint();
+ mFrequencyResponseHighlight2.setStyle(Style.STROKE);
+ mFrequencyResponseHighlight2.setStrokeWidth(3);
+ mFrequencyResponseHighlight2.setColor(getResources().getColor(R.color.freq_hl2));
+ mFrequencyResponseHighlight2.setAntiAlias(true);
+ }
+
+ /**
+ * Listener for bands being modified via touch events
+ *
+ * Invoked with the index of the modified band, and the
+ * new value in dB.
+ */
+ public interface BandUpdatedListener {
+ public void onBandUpdated(int band, float dB);
+ }
+
+ public void setBandLevelRange(float minDB, float maxDB) {
+ mMinDB = minDB;
+ mMaxDB = maxDB;
+ }
+
+ public void setCenterFreqs(float[] centerFreqsKHz) {
+ mNumBands = centerFreqsKHz.length;
+ mLevels = new float[mNumBands];
+ mCenterFreqs = Arrays.copyOf(centerFreqsKHz, mNumBands);
+ System.arraycopy(centerFreqsKHz, 0, mCenterFreqs, 0, mNumBands);
+ mMinFreq = mCenterFreqs[0] / 2;
+ mMaxFreq = (float) Math.pow(mCenterFreqs[mNumBands - 1], 2) / mCenterFreqs[mNumBands -2] / 2;
+ }
+
+ /*
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Bundle b = new Bundle();
+ b.putParcelable("super", super.onSaveInstanceState());
+ b.putFloatArray("levels", mLevels);
+ return b;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable p) {
+ Bundle b = (Bundle) p;
+ super.onRestoreInstanceState(b.getBundle("super"));
+ mLevels = b.getFloatArray("levels");
+ }
+ */
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ buildLayer();
+ }
+
+ /**
+ * Returns a color that is assumed to be blended against black background,
+ * assuming close to sRGB behavior of screen (gamma 2.2 approximation).
+ *
+ * @param intensity desired physical intensity of color component
+ * @param alpha alpha value of color component
+ */
+ private static int gamma(float intensity, float alpha) {
+ /* intensity = (component * alpha)^2.2
+ * <=>
+ * intensity^(1/2.2) / alpha = component
+ */
+
+ double gamma = Math.round(255 * Math.pow(intensity, 1 / 2.2) / alpha);
+ return (int) Math.min(255, Math.max(0, gamma));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ final Resources res = getResources();
+ mWidth = right - left;
+ mHeight = bottom - top;
+
+ float barWidth = res.getDimensionPixelSize(R.dimen.eq_bar_width);
+ mControlBar.setStrokeWidth(barWidth);
+
+ /**
+ * red > +7
+ * yellow > +3
+ * holo_blue_bright > 0
+ * holo_blue < 0
+ * holo_blue_dark < 3
+ */
+ int[] responseColors = new int[] {
+ res.getColor(R.color.eq_red),
+ res.getColor(R.color.eq_yellow),
+ res.getColor(R.color.eq_holo_bright),
+ res.getColor(R.color.eq_holo_blue),
+ res.getColor(R.color.eq_holo_dark)
+ };
+ float[] responsePositions = new float[] {
+ 0, 0.2f, 0.45f, 0.6f, 1f
+ };
+
+ mFrequencyResponseBg.setShader(new LinearGradient(0, 0, 0, mHeight,
+ responseColors, responsePositions, Shader.TileMode.CLAMP));
+
+ int[] barColors = new int[] {
+ res.getColor(R.color.cb_shader),
+ res.getColor(R.color.cb_shader_alpha)
+ };
+ float[] barPositions = new float[] {
+ 0, 1
+ };
+
+ mControlBar.setShader(new LinearGradient(0, 0, 0, mHeight,
+ barColors, barPositions, Shader.TileMode.CLAMP));
+ }
+
+ public void setBand(int i, float value) {
+ mLevels[i] = value;
+ postInvalidate();
+ }
+
+ public float getBand(int i) {
+ return mLevels[i];
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ /* clear canvas */
+ canvas.drawRGB(0, 0, 0);
+
+ Biquad[] biquads = new Biquad[mNumBands - 1];
+ for (int i = 0; i < (mNumBands - 1); i++) {
+ biquads[i] = new Biquad();
+ }
+
+ /* The filtering is realized with 2nd order high shelf filters, and each band
+ * is realized as a transition relative to the previous band. The center point for
+ * each filter is actually between the bands.
+ *
+ * 1st band has no previous band, so it's just a fixed gain.
+ */
+ double gain = Math.pow(10, mLevels[0] / 20);
+ for (int i = 0; i < biquads.length; i++) {
+ biquads[i].setHighShelf(mCenterFreqs[i], SAMPLING_RATE, mLevels[i + 1] - mLevels[i], 1);
+ }
+
+ Path freqResponse = new Path();
+ Complex[] zn = new Complex[biquads.length];
+ for (int i = 0; i < 71; i ++) {
+ double freq = reverseProjectX(i / 70f);
+ double omega = freq / SAMPLING_RATE * Math.PI * 2;
+ Complex z = new Complex(Math.cos(omega), Math.sin(omega));
+
+ /* Evaluate the response at frequency z */
+ /* Complex z1 = z.mul(gain); */
+ double lin = gain;
+ for (int j = 0; j < biquads.length; j++) {
+ zn[j] = biquads[j].evaluateTransfer(z);
+ lin *= zn[j].rho();
+ }
+
+ /* Magnitude response, dB */
+ double dB = lin2dB(lin);
+ float x = projectX(freq) * mWidth;
+ float y = projectY(dB) * mHeight;
+
+ /* Set starting point at first point */
+ if (i == 0) {
+ freqResponse.moveTo(x, y);
+ } else {
+ freqResponse.lineTo(x, y);
+ }
+ }
+
+ Path freqResponseBg = new Path();
+ freqResponseBg.addPath(freqResponse);
+ freqResponseBg.offset(0, -4);
+ freqResponseBg.lineTo(mWidth, mHeight);
+ freqResponseBg.lineTo(0, mHeight);
+ freqResponseBg.close();
+ canvas.drawPath(freqResponseBg, mFrequencyResponseBg);
+
+ canvas.drawPath(freqResponse, mFrequencyResponseHighlight);
+ canvas.drawPath(freqResponse, mFrequencyResponseHighlight2);
+
+ /* draw vertical lines */
+ for (float freq = mMinFreq; freq < mMaxFreq;) {
+ float x = projectX(freq) * mWidth;
+ canvas.drawLine(x, 0, x, mHeight - 1, mGridLines);
+ if (freq < 100) {
+ freq += 10;
+ } else if (freq < 1000) {
+ freq += 100;
+ } else if (freq < 10000) {
+ freq += 1000;
+ } else {
+ freq += 10000;
+ }
+ }
+
+ /* draw horizontal lines */
+ for (float dB = mMinDB + 3; dB <= mMaxDB - 3; dB += 3) {
+ float y = projectY(dB) * mHeight;
+ canvas.drawLine(0, y, mWidth - 1, y, mGridLines);
+ canvas.drawText(String.format("%+d", (int)dB), 1, (y - 1), mWhite);
+ }
+
+ for (int i = 0; i < mNumBands; i ++) {
+ float freq = mCenterFreqs[i];
+ float x = projectX(freq) * mWidth;
+ float y = projectY(mLevels[i]) * mHeight;
+ String frequencyText = String.format(freq < 1000 ? "%.0f" : "%.0fk",
+ freq < 1000 ? freq : freq / 1000);
+
+ canvas.drawLine(x, mHeight, x, y, mControlBar);
+ canvas.drawText(String.format("%+1.1f", mLevels[i]), x, mHeight - 2, mControlBarText);
+ canvas.drawText(frequencyText, x, mWhite.getTextSize(), mControlBarText);
+ }
+ }
+
+ public void registerBandUpdatedListener(BandUpdatedListener listener) {
+ mBandUpdatedListener = listener;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+
+ if (mReadOnly)
+ return false;
+
+ float x = event.getX();
+ float y = event.getY();
+
+ /* Which band is closest to the position user pressed? */
+ int band = findClosest(x);
+
+ int wy = getHeight();
+ float level = (y / wy) * (mMinDB - mMaxDB) - mMinDB;
+ if (level < mMinDB) {
+ level = mMinDB;
+ } else if (level > mMaxDB) {
+ level = mMaxDB;
+ }
+
+ setBand(band, level);
+
+ if (mBandUpdatedListener != null) {
+ mBandUpdatedListener.onBandUpdated(band, level);
+ }
+
+ return true;
+ }
+
+ private float projectX(double freq) {
+ double pos = Math.log(freq);
+ double minPos = Math.log(mMinFreq);
+ double maxPos = Math.log(mMaxFreq);
+ return (float) ((pos - minPos) / (maxPos - minPos));
+ }
+
+ private double reverseProjectX(float pos) {
+ double minPos = Math.log(mMinFreq);
+ double maxPos = Math.log(mMaxFreq);
+ return Math.exp(pos * (maxPos - minPos) + minPos);
+ }
+
+ private float projectY(double dB) {
+ double pos = (dB - mMinDB) / (mMaxDB - mMinDB);
+ return (float) (1 - pos);
+ }
+
+ private double lin2dB(double rho) {
+ return rho != 0 ? Math.log(rho) / Math.log(10) * 20 : -99.9;
+ }
+
+ /**
+ * Find the closest control to given horizontal pixel for adjustment
+ *
+ * @param px
+ * @return index of best match
+ */
+ public int findClosest(float px) {
+ int idx = 0;
+ float best = 1e9f;
+ for (int i = 0; i < mNumBands; i ++) {
+ float freq = mCenterFreqs[i];
+ float cx = projectX(freq) * mWidth;
+ float distance = Math.abs(cx - px);
+
+ if (distance < best) {
+ idx = i;
+ best = distance;
+ }
+ }
+
+ return idx;
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/widget/Gallery.java b/src/org/cyanogenmod/audiofx/widget/Gallery.java
new file mode 100644
index 0000000..8bb5f2a
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/widget/Gallery.java
@@ -0,0 +1,120 @@
+/*
+ * 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 org.cyanogenmod.audiofx.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.TextView;
+
+import org.cyanogenmod.audiofx.R;
+
+public class Gallery extends android.widget.Gallery {
+ public interface OnItemSelectedListener {
+ public void onItemSelected(int position);
+ }
+
+ private boolean mEnabled = false;
+
+ private int mHighlightColor;
+ private int mLowlightColor;
+ private int mDisabledColor;
+
+ private TextView mLastView = null;
+ private OnItemSelectedListener mOnItemSelectedListener = null;
+
+ public Gallery(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ Resources res = getResources();
+ mHighlightColor = res.getColor(R.color.highlight);
+ mLowlightColor = res.getColor(R.color.grey);
+ mDisabledColor = res.getColor(R.color.disabled_gallery);
+
+ setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ TextView tv = (TextView) view;
+ if (tv != null) {
+ tv.setTextColor(mEnabled ? mHighlightColor : mDisabledColor);
+ }
+ if (mLastView != null && mLastView != tv) {
+ mLastView.setTextColor(mEnabled ? mLowlightColor : mDisabledColor);
+ }
+ mLastView = tv;
+ if (mEnabled && mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onItemSelected(position);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+ }
+
+ public Gallery(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public Gallery(Context context) {
+ this(context, null);
+ }
+
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ final int count = getChildCount();
+ for (int i = 0; i < count; ++i) {
+ final View view = getChildAt(i);
+ if (view instanceof TextView) {
+ ((TextView) view).setTextColor(enabled ? mLowlightColor : mDisabledColor);
+ }
+ }
+
+ if (enabled) {
+ final TextView tv = (TextView) getSelectedView();
+ if (tv != null) {
+ tv.setTextColor(mHighlightColor);
+ }
+ }
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return mEnabled ? super.onDown(e) : false;
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java b/src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java
new file mode 100644
index 0000000..c8d838a
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java
@@ -0,0 +1,59 @@
+/*
+ * 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 org.cyanogenmod.audiofx.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+
+public class InterceptableLinearLayout extends LinearLayout {
+ private boolean mIntercept = true;
+
+ public InterceptableLinearLayout(Context context) {
+ super(context);
+ }
+
+ public InterceptableLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InterceptableLinearLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return mIntercept;
+ }
+
+ public void setInterception(boolean intercept) {
+ mIntercept = intercept;
+ }
+}
diff --git a/src/org/cyanogenmod/audiofx/widget/Knob.java b/src/org/cyanogenmod/audiofx/widget/Knob.java
new file mode 100644
index 0000000..874d965
--- /dev/null
+++ b/src/org/cyanogenmod/audiofx/widget/Knob.java
@@ -0,0 +1,333 @@
+/*
+ * 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 org.cyanogenmod.audiofx.widget;
+
+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 org.cyanogenmod.audiofx.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 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;
+ }
+}