diff options
author | Steve Kondik <shade@chemlab.org> | 2014-04-08 10:52:21 -0700 |
---|---|---|
committer | Steve Kondik <shade@chemlab.org> | 2014-04-08 10:52:21 -0700 |
commit | 3396e4fd07c7741bce6ff719295f8adf2c9e1869 (patch) | |
tree | 67f8d83ae6bafaf01f17a337473c9ad2e19f1fb1 /src/org | |
parent | 6ae11e2ccdb97317e29f2ebfa373e3fc744d7fcc (diff) | |
download | android_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.java | 707 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/Compatibility.java | 244 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/ControlPanelEffect.java | 1490 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/ControlPanelPicker.java | 121 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/ControlPanelReceiver.java | 105 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/OpenSLESConstants.java | 124 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/seekbar/AbsSeekBar.java | 561 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/seekbar/ProgressBar.java | 1146 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/seekbar/SeekBar.java | 121 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/widget/Biquad.java | 31 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/widget/Complex.java | 94 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java | 401 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/widget/Gallery.java | 120 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java | 59 | ||||
-rw-r--r-- | src/org/cyanogenmod/audiofx/widget/Knob.java | 333 |
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 < 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 <ProgressBar>} 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> + * <ProgressBar + * style="@android:style/Widget.ProgressBar.Horizontal" + * ... /></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—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—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> + * <LinearLayout + * android:orientation="horizontal" + * ... > + * <ProgressBar + * android:layout_width="wrap_content" + * android:layout_height="wrap_content" + * style="@android:style/Widget.ProgressBar.Small" + * android:layout_marginRight="5dp" /> + * <TextView + * android:layout_width="wrap_content" + * android:layout_height="wrap_content" + * android:text="@string/loading" /> + * </LinearLayout></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; + } +} |