diff options
Diffstat (limited to 'src/org/cyanogenmod/audiofx')
48 files changed, 8112 insertions, 2247 deletions
diff --git a/src/org/cyanogenmod/audiofx/ActivityMusic.java b/src/org/cyanogenmod/audiofx/ActivityMusic.java deleted file mode 100644 index 883b127..0000000 --- a/src/org/cyanogenmod/audiofx/ActivityMusic.java +++ /dev/null @@ -1,789 +0,0 @@ -/* - * 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. - */ - -package org.cyanogenmod.audiofx; - -import android.app.ActionBar; -import android.app.Activity; -import android.content.*; -import android.graphics.drawable.ColorDrawable; -import android.media.audiofx.AudioEffect; -import android.media.audiofx.AudioEffect.Descriptor; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.*; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.CompoundButton.OnCheckedChangeListener; -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 java.util.UUID; - -/** - * - */ -public class ActivityMusic extends Activity { - - private final static String TAG = "AudioFXActivityMusic"; - private final static boolean DEBUG = false; - - /** - * 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; - private ServiceConnection mServiceConnection; - - // Equalizer fields - private int mNumberEqualizerBands; - private int mEQCustomPresetPosition = 1; - private int mEQPreset; - private String[] mEQPresetNames; - private String[] mReverbPresetNames; - - private int mPRPreset; - - private boolean mEQAnimatingToUserPos = false; - - private ViewGroup mContentEffectsViewGroup; - private EqualizerSurface mEqualizerSurface; - private Gallery mEqGallery; - private Gallery mReverbGallery; - private Knob mVirtualizerKnob; - private Knob mBassKnob; - - private boolean mKnobsAvailable = false; - private Switch mToggleSwitch; - - private boolean mStandalone = false; - private boolean mStateChangeUpdate = false; - - private Toast mCurrentToast; - - HeadsetService mService; - - private String mCurrentDevice = "speaker"; // the sensible default - - 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 - }; - - private Context mContext; - - private int mAudioSession = AudioEffect.ERROR_BAD_VALUE; - - private static final int MSG_UPDATE_EQ = 1; - private static final int MSG_UPDATE_SERVICE = 2; - private static final int MSG_UPDATE_EQ_ANIMATE = 3; - Handler mHandler = new Handler() { - - @Override - public void handleMessage(Message msg) { - super.handleMessage(msg); - switch (msg.what) { - case MSG_UPDATE_EQ: - equalizerUpdateDisplayInternal(false); - break; - case MSG_UPDATE_SERVICE: - if (mService != null) { - mService.update(); - } - break; - case MSG_UPDATE_EQ_ANIMATE: - equalizerUpdateDisplayInternal(true); - break; - } - } - }; - - // 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(); - - if (action.equals(HeadsetService.ACTION_UPDATE_PREFERENCES)) { - if (mCurrentDeviceOverride == false) { // the user has selected a device, don't interrupt them. - if (mService != null) { - mCurrentDevice = mService.getAudioOutputRouting(); - } - } - - updateUI(true); - mStateChangeUpdate = true; - getActionBar().setSelectedNavigationItem(getCurrentDeviceIndex()); - equalizerSetPreset(mEQPreset); - equalizerUpdateDisplay(true); - } - } - }; - private ArrayAdapter<String> mNavBarDeviceAdapter; - - private boolean mCurrentDeviceOverride = false; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - startService(new Intent(this, HeadsetService.class)); - - // 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); - - // check for errors - if (getCallingPackage() == null) { - mStandalone = true; - } else { - mStandalone = false; - } - setResult(RESULT_OK); - - // query available effects - final Descriptor[] effects = AudioEffect.queryEffects(); - - // Determine available/supported effects - if (DEBUG) Log.v(TAG, "Available effects:"); - for (final Descriptor effect : effects) { - if (DEBUG) 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; - } - } - - setContentView(R.layout.music_main); - - mContentEffectsViewGroup = (ViewGroup) findViewById(R.id.contentSoundEffects); - - // fix up labels - TextView reverbLabel = (TextView) findViewById(R.id.reverb_label); - reverbLabel.setText("- " + reverbLabel.getText() + " -"); - - TextView eqPresetLabel = (TextView) findViewById(R.id.eq_preset_label); - eqPresetLabel.setText("- " + eqPresetLabel.getText() + " -"); - - // setup actionbar on off switch - mToggleSwitch = new Switch(this); - mToggleSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(final CompoundButton buttonView, - final boolean isChecked) { - // set parameter and state - getPrefs().edit().putBoolean("audiofx.global.enable", isChecked).apply(); - - updateUI(true); - setInterception(isChecked); - updateService(); - } - }); - - // setup action bar - String[] navigationBarDevices = new String[HeadsetService.DEFAULT_AUDIO_DEVICES.length]; - for (int i = 0; i < navigationBarDevices.length; i++) { - navigationBarDevices[i] = localizeDevice(HeadsetService.DEFAULT_AUDIO_DEVICES[i]); - } - - mNavBarDeviceAdapter = new ArrayAdapter<String>(getBaseContext(), android.R.layout.simple_spinner_dropdown_item, - navigationBarDevices); - ActionBar.OnNavigationListener navigationListener = new ActionBar.OnNavigationListener() { - @Override - public boolean onNavigationItemSelected(int itemPosition, long itemId) { - if (mStateChangeUpdate) { - mStateChangeUpdate = false; - } else { - mCurrentDeviceOverride = true; - mCurrentDevice = HeadsetService.DEFAULT_AUDIO_DEVICES[itemPosition]; - } - updateUI(true); - equalizerSetPreset(mEQPreset); - equalizerUpdateDisplay(true); - mBassKnob.setValue(Integer.valueOf(getPrefs().getString("audiofx.bass.strength", "0"))); - mVirtualizerKnob.setValue(Integer.valueOf(getPrefs().getString("audiofx.virtualizer.strength", "0"))); - return true; - } - }; - - ActionBar ab = getActionBar(); - final ActionBar.LayoutParams params = new ActionBar.LayoutParams( - ActionBar.LayoutParams.WRAP_CONTENT, - ActionBar.LayoutParams.WRAP_CONTENT, - Gravity.CENTER_VERTICAL | Gravity.END); - - ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); - ab.setListNavigationCallbacks(mNavBarDeviceAdapter, navigationListener); - ab.setBackgroundDrawable(new ColorDrawable(getResources() - .getColor(R.color.action_bar_background))); - mStateChangeUpdate = true; - ab.setSelectedNavigationItem(getCurrentDeviceIndex()); - - ab.setCustomView(mToggleSwitch, params); - ab.setDisplayShowTitleEnabled(false); - ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - - // initialize views - mEqualizerSurface = (EqualizerSurface) findViewById(R.id.frequencyResponse); - mEqGallery = (Gallery) findViewById(R.id.eqPresets); - mReverbGallery = (Gallery) findViewById(R.id.reverb_gallery); - mVirtualizerKnob = (Knob) findViewById(R.id.vIStrengthKnob); - mBassKnob = (Knob) findViewById(R.id.bBStrengthKnob); - - // setup equalizer presets - final int numPresets = Integer.parseInt(getSharedPreferences("global", 0) - .getString("equalizer.number_of_presets", "0")); - mEQPresetNames = new String[numPresets + 3]; - - String[] presetNames = getSharedPreferences("global", 0).getString("equalizer.preset_names", "").split("\\|"); - for (short i = 0; i < numPresets; i++) { - mEQPresetNames[i] = localizePresetName(presetNames[i]); - } - mEQPresetNames[numPresets] = getString(R.string.electronic); - mEQPresetNames[numPresets + 1] = getString(R.string.small_speakers); - mEQPresetNames[numPresets + 2] = getString(R.string.custom); - mEQCustomPresetPosition = numPresets + 2; - - ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.equalizer_presets, - mEQPresetNames); - - mEqGallery.setAdapter(adapter); - mEqGallery.setSelection(mEQPreset); - mEqGallery.setOnItemSelectedListener(new Gallery.OnItemSelectedListener() { - @Override - public void onItemSelected(int position) { - mEQPreset = position; - if (!mEQAnimatingToUserPos) { - equalizerSetPreset(position); - } else if (mEQAnimatingToUserPos && mEQPreset == mEQCustomPresetPosition) { - mEQAnimatingToUserPos = false; - } - } - }); - - // setup equalizer - mNumberEqualizerBands = Integer.parseInt(getSharedPreferences("global", 0) - .getString("equalizer.number_of_bands", "5")); - final int[] centerFreqs = getCenterFreqs(); - final int[] bandLevelRange = getBandLevelRange(); - float[] centerFreqsKHz = new float[centerFreqs.length]; - for (int i = 0; i < centerFreqs.length; i++) { - centerFreqsKHz[i] = (float) centerFreqs[i] / 1000.0f; - } - mEqualizerSurface.setCenterFreqs(centerFreqsKHz); - mEqualizerSurface.setBandLevelRange(bandLevelRange[0] / 100, bandLevelRange[1] / 100); - final EqualizerSurface.BandUpdatedListener listener = new EqualizerSurface.BandUpdatedListener() { - - @Override - public void onBandUpdated(int band, float dB) { - if (mEQPreset != mEQCustomPresetPosition && !mEQAnimatingToUserPos) { - equalizerCopyToCustom(); - mEQAnimatingToUserPos = true; - mEqGallery.setAnimationDuration(1000); - mEqGallery.setSelection(mEQCustomPresetPosition, true); - } else { - equalizerBandUpdate(band, (int) (dB * 100)); - } - } - - float[] animatingLevels; - - @Override - public void onBandAnimating(int band, float dB) { - if (animatingLevels == null) { - animatingLevels = mEqualizerSurface.softCopyLevels(); - } - animatingLevels[band] = dB; - if (mService != null) { - mService.setEqualizerLevels(animatingLevels); - } - mHandler.sendEmptyMessage(MSG_UPDATE_SERVICE); - } - - @Override - public void onBandAnimationCompleted() { - if (mService != null) { - mService.setEqualizerLevels(animatingLevels = null); - } - mHandler.sendEmptyMessage(MSG_UPDATE_SERVICE); - } - }; - mEqualizerSurface.registerBandUpdatedListener(listener); - - // setup virtualizer knob - mVirtualizerKnob.setMax(OpenSLESConstants.VIRTUALIZER_MAX_STRENGTH - - OpenSLESConstants.VIRTUALIZER_MIN_STRENGTH); - mVirtualizerKnob.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) { - if (fromUser) { - // set parameter and state - getPrefs().edit().putBoolean("audiofx.virtualizer.enable", true).apply(); - getPrefs().edit().putString("audiofx.virtualizer.strength", String.valueOf(value)).apply(); - mHandler.sendEmptyMessage(MSG_UPDATE_SERVICE); - } - } - - @Override - public boolean onSwitchChanged(final Knob knob, boolean on) { - if (!mKnobsAvailable) { - showHeadsetMsg(); - return false; - } -// knob.setOn(getPrefs().getBoolean("audiofx.virtualizer.enable", true)); -// knob.setOn(on); - return true; - } - - @Override - public void onAnimationFinished(boolean endValue) { - getPrefs().edit().putBoolean("audiofx.virtualizer.enable", endValue).apply(); -// updateService(); - mHandler.sendEmptyMessage(MSG_UPDATE_SERVICE); - } - }); - - // setup bass knob - mBassKnob.setMax(OpenSLESConstants.BASSBOOST_MAX_STRENGTH - - OpenSLESConstants.BASSBOOST_MIN_STRENGTH); - mBassKnob.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) { - if (fromUser) { - // set parameter and state - getPrefs().edit().putBoolean("audiofx.bass.enable", true).apply(); - getPrefs().edit().putString("audiofx.bass.strength", String.valueOf(value)).apply(); - mHandler.sendEmptyMessage(MSG_UPDATE_SERVICE); - } - } - - @Override - public boolean onSwitchChanged(final Knob knob, boolean on) { - if (!mKnobsAvailable) { - showHeadsetMsg(); - return false; - } -// knob.setOn(on); - return true; - } - - @Override - public void onAnimationFinished(boolean endValue) { - getPrefs().edit().putBoolean("audiofx.bass.enable", endValue).apply(); -// updateService(); - mHandler.sendEmptyMessage(MSG_UPDATE_SERVICE); - } - }); - - - // setup reverb presets - mReverbPresetNames = new String[mReverbPresetRSids.length]; - for (short i = 0; i < mReverbPresetRSids.length; ++i) { - mReverbPresetNames[i] = getString(mReverbPresetRSids[i]); - } - - ArrayAdapter<String> reverbAdapter = new ArrayAdapter<String>(this, - R.layout.equalizer_presets, mReverbPresetNames); - mReverbGallery.setAdapter(reverbAdapter); - mReverbGallery.setOnItemSelectedListener(new OnItemSelectedListener() { - - @Override - public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - if (position != mPRPreset) { - presetReverbSetPreset(position); - } - mPRPreset = position; - } - - @Override - public void onNothingSelected(AdapterView<?> parent) { - } - }); - mReverbGallery.setSelection(mPRPreset); - - - } - - private SharedPreferences getPrefs() { - return getSharedPreferences(mCurrentDevice, 0); - } - - 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; - } - - private final String localizeDevice(String device) { - return getString(mContext.getResources().getIdentifier("device_" + device, "string", getPackageName())); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - updateUI(false); - } - - @Override - protected void onResume() { - super.onResume(); - - if (mServiceConnection == null) { - mServiceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder binder) { - mService = ((HeadsetService.LocalBinder) binder).getService(); - if (!mCurrentDeviceOverride) { - mCurrentDevice = mService.getAudioOutputRouting(); - } - updateUI(true); - - mStateChangeUpdate = true; - getActionBar().setSelectedNavigationItem(getCurrentDeviceIndex()); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - mService = null; - } - }; - } - Intent serviceIntent = new Intent(this, HeadsetService.class); - bindService(serviceIntent, mServiceConnection, 0); - - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(HeadsetService.ACTION_UPDATE_PREFERENCES); - registerReceiver(mReceiver, intentFilter); - - equalizerUpdateDisplay(true); - } - - @Override - protected void onPause() { - super.onPause(); - - // clear the toast - if (mCurrentToast != null) { - mCurrentToast.cancel(); - mCurrentToast = null; - } - - unbindService(mServiceConnection); - - // 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 updateUI(boolean fromNavbar) { - if (!fromNavbar) { - mStateChangeUpdate = true; - getActionBar().setSelectedNavigationItem(getCurrentDeviceIndex()); - } - - boolean isSpeaker = mCurrentDevice.equals("speaker"); - - final boolean isEnabled = getPrefs().getBoolean("audiofx.global.enable", isSpeaker); - mKnobsAvailable = !isSpeaker; - - mToggleSwitch.setChecked(isEnabled); - - if (mVirtualizerSupported) { - mVirtualizerKnob.setOn(getPrefs().getBoolean("audiofx.virtualizer.enable", false), false); - mVirtualizerKnob.setEnabled(isEnabled && mKnobsAvailable); - } else { - mVirtualizerKnob.setVisibility(View.GONE); - } - if (mBassBoostSupported) { - mBassKnob.setOn(getPrefs().getBoolean("audiofx.bass.enable", true), false); - mBassKnob.setEnabled(isEnabled && mKnobsAvailable); - } else { - mBassKnob.setVisibility(View.GONE); - } - if (mEqualizerSupported) { - String preset; - if (isSpeaker) { - preset = String.valueOf(mNumberEqualizerBands + 2); - } else { - preset = "3"; - } - mEQPreset = Integer.valueOf(getPrefs().getString("audiofx.eq.preset", preset)); - mEqGallery.setEnabled(isEnabled); - mEqGallery.setSelection(mEQPreset); - } - if (mPresetReverbSupported) { - mPRPreset = Integer.valueOf(getPrefs().getString("audiofx.reverb.preset", "0")); - mReverbGallery.setSelection(mPRPreset, true); - mReverbGallery.setEnabled(isEnabled); - } - - setInterception(isEnabled); - } - - private void updateUI() { - updateUI(false); - } - - private int getCurrentDeviceIndex() { - for (int i = 0; i < HeadsetService.DEFAULT_AUDIO_DEVICES.length; i++) { - if (HeadsetService.DEFAULT_AUDIO_DEVICES[i].equals(mCurrentDevice)) { - return i; - } - } - return 0; - } - - 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) { - // clear the toast - if (mCurrentToast != null) { - mCurrentToast.cancel(); - mCurrentToast = null; - } - mCurrentToast = Toast.makeText(mContext, - getString(R.string.power_on_prompt), Toast.LENGTH_SHORT); - mCurrentToast.setGravity(Gravity.CENTER, 0, 0); - mCurrentToast.show(); - } - }); - } - } - - private int[] getBandLevelRange() { - String savedCenterFreqs = getSharedPreferences("global", 0).getString("equalizer.band_level_range", null); - if (savedCenterFreqs == null || savedCenterFreqs.isEmpty()) { - return new int[]{-1500, 1500}; - } else { - String[] split = savedCenterFreqs.split(";"); - int[] freqs = new int[split.length]; - for (int i = 0; i < split.length; i++) { - freqs[i] = Integer.valueOf(split[i]); - } - return freqs; - } - } - - private int[] getCenterFreqs() { - String savedCenterFreqs = getSharedPreferences("global", 0).getString("equalizer.center_freqs", - HeadsetService.getZeroedBandsString(mNumberEqualizerBands)); - String[] split = savedCenterFreqs.split(";"); - int[] freqs = new int[split.length]; - for (int i = 0; i < split.length; i++) { - freqs[i] = Integer.valueOf(split[i]); - } - return freqs; - } - - /** - * Updates the EQ by getting the parameters. - */ - private void equalizerUpdateDisplay(boolean animate) { - mHandler.removeMessages(animate ? MSG_UPDATE_EQ_ANIMATE : MSG_UPDATE_EQ); - mHandler.sendEmptyMessageDelayed(animate ? MSG_UPDATE_EQ_ANIMATE : MSG_UPDATE_EQ, 100); - } - - private void equalizerUpdateDisplayInternal(boolean animate) { - String levelsString = null; - float[] floats; - - if (mEQPreset == mEQCustomPresetPosition) { - // load custom preset for current device - // here mEQValues needs to be pre-populated with the user's preset values. - String[] customEq = getPrefs().getString("audiofx.eq.bandlevels.custom", - HeadsetService.getZeroedBandsString(mNumberEqualizerBands)).split(";"); - floats = new float[mNumberEqualizerBands]; - for (int band = 0; band < floats.length; band++) { - final float level = Float.parseFloat(customEq[band]); - floats[band] = level / 100.0f; - } - if (animate) { - mEqualizerSurface.setBands(floats); - } else { - for (int band = 0; band < mNumberEqualizerBands; band++) { - mEqualizerSurface.setBand(band, (float) floats[band] / 100.0f); - } - } - } else { - // try to load preset - levelsString = getSharedPreferences("global", 0).getString("equalizer.preset." + mEQPreset, - HeadsetService.getZeroedBandsString(mNumberEqualizerBands)); - String[] bandLevels = levelsString.split(";"); - floats = new float[bandLevels.length]; - for (int band = 0; band < bandLevels.length; band++) { - final float level = Float.parseFloat(bandLevels[band]); - floats[band] = level / 100.0f; - if (!animate) { - mEqualizerSurface.setBand(band, (float) level / 100.0f); - } - } - if (animate) { - mEqualizerSurface.setBands(floats); - } - } - } - - /** - * Called when user starts touch eq on a preset - */ - private void equalizerCopyToCustom() { - if (DEBUG) Log.d(TAG, "equalizerCopyToCustom()"); - StringBuilder bandLevels = new StringBuilder(); - for (int band = 0; band < mNumberEqualizerBands; band++) { - final float level = mEqualizerSurface.getBand(band); - bandLevels.append(level * 100); - bandLevels.append(";"); - } - // remove trailing ";" - bandLevels.deleteCharAt(bandLevels.length() - 1); - getPrefs().edit().putString("audiofx.eq.bandlevels.custom", bandLevels.toString()).apply(); - getPrefs().edit().putString("audiofx.eq.preset", String.valueOf(mEQCustomPresetPosition)).apply(); - } - - private void equalizerBandUpdate(final int band, final int level) { - if (DEBUG) Log.d(TAG, "equalizerBandUpdate(band: " + band + ", level: " + level + ")"); - - String[] currentCustomLevels = getPrefs().getString("audiofx.eq.bandlevels.custom", - HeadsetService.getZeroedBandsString(mNumberEqualizerBands)).split(";"); - - currentCustomLevels[band] = String.valueOf(level); - // save - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < mNumberEqualizerBands; i++) { - builder.append(currentCustomLevels[i]); - builder.append(";"); - } - builder.deleteCharAt(builder.length() - 1); - getPrefs().edit().putString("audiofx.eq.bandlevels", builder.toString()).apply(); - getPrefs().edit().putString("audiofx.eq.bandlevels.custom", builder.toString()).apply(); - - updateService(); - } - - private void updateService() { - mHandler.removeMessages(MSG_UPDATE_SERVICE); - mHandler.sendEmptyMessageDelayed(MSG_UPDATE_SERVICE, 100); - } - - private void equalizerSetPreset(final int preset) { - if (DEBUG) Log.d(TAG, "equalizerSetPreset(" + preset + ")"); - - mEQPreset = preset; - getPrefs().edit().putString("audiofx.eq.preset", String.valueOf(preset)).apply(); - - String newLevels = null; - if (preset == mEQCustomPresetPosition) { - // load custom if possible - newLevels = getPrefs().getString("audiofx.eq.bandlevels.custom", - HeadsetService.getZeroedBandsString(mNumberEqualizerBands)); - } else { - newLevels = getSharedPreferences("global", 0).getString("equalizer.preset." + preset, - HeadsetService.getZeroedBandsString(mNumberEqualizerBands)); - } - getPrefs().edit().putString("audiofx.eq.bandlevels", newLevels).apply(); - equalizerUpdateDisplay(true); - - updateService(); - } - - - private void presetReverbSetPreset(final int preset) { - getPrefs().edit().putString("audiofx.reverb.preset", String.valueOf(preset)).apply(); - updateService(); - } - - private void showHeadsetMsg() { - // clear the toast - if (mCurrentToast != null) { - mCurrentToast.cancel(); - mCurrentToast = null; - } - - final Context context = getApplicationContext(); - final int duration = Toast.LENGTH_SHORT; - - mCurrentToast = Toast.makeText(context, getString(R.string.effect_unavalable_for_speaker), duration); - mCurrentToast.setGravity(Gravity.CENTER, mCurrentToast.getXOffset() / 2, mCurrentToast.getYOffset() / 2); - mCurrentToast.show(); - } - -} diff --git a/src/org/cyanogenmod/audiofx/BootReceiver.java b/src/org/cyanogenmod/audiofx/BootReceiver.java deleted file mode 100644 index b032310..0000000 --- a/src/org/cyanogenmod/audiofx/BootReceiver.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.cyanogenmod.audiofx; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Created by roman on 5/12/14. - */ -public class BootReceiver extends BroadcastReceiver { - public void onReceive(Context context, Intent intent) { - context.startService(new Intent(context, HeadsetService.class)); - } -} diff --git a/src/org/cyanogenmod/audiofx/HeadsetService.java b/src/org/cyanogenmod/audiofx/HeadsetService.java deleted file mode 100644 index 98bf20e..0000000 --- a/src/org/cyanogenmod/audiofx/HeadsetService.java +++ /dev/null @@ -1,708 +0,0 @@ -/* - * 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. - */ -package org.cyanogenmod.audiofx; - -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.media.AudioManager; -import android.media.AudioPatch; -import android.media.AudioPort; -import android.media.AudioSystem; -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.os.Binder; -import android.os.IBinder; -import android.util.Log; -import android.util.SparseArray; - -import java.util.Arrays; - -import cyanogenmod.media.AudioSessionInfo; -import cyanogenmod.media.CMAudioManager; - -/** - * <p>This calls listen to events that affect DSP function and responds to them.</p> - * <ol> - * <li>new audio session declarations</li> - * <li>headset plug / unplug events</li> - * <li>preference update events.</li> - * </ol> - * - * @author alankila - */ -public class HeadsetService extends Service { - - public static final String ACTION_UPDATE_PREFERENCES = "org.cyanogenmod.audiofx.UPDATE_PREFS"; - public static final String[] DEFAULT_AUDIO_DEVICES = new String[]{ - "headset", "speaker", "usb", "bluetooth", "wireless", "lineout" - }; - - static String getZeroedBandsString(int length) { - StringBuffer buff = new StringBuffer(); - for (int i = 0; i < length; i++) { - buff.append("0;"); - } - buff.deleteCharAt(buff.length() - 1); - return buff.toString(); - } - - /** - * Helper class representing the full complement of effects attached to one - * audio session. - * - * @author alankila - */ - private static class EffectSet { - /** - * Session-specific equalizer - */ - private final Equalizer mEqualizer; - /** - * Session-specific bassboost - */ - private final BassBoost mBassBoost; - /** - * Session-specific virtualizer - */ - private final Virtualizer mVirtualizer; - - private final PresetReverb mPresetReverb; - - private short mEqNumPresets = -1; - private short mEqNumBands = -1; - - public EffectSet(int sessionId) { - mEqualizer = new Equalizer(0, sessionId); - mBassBoost = new BassBoost(0, sessionId); - mVirtualizer = new Virtualizer(0, sessionId); - mPresetReverb = new PresetReverb(0, sessionId); - } - - /* - * Take lots of care to not poke values that don't need - * to be poked- this can cause audible pops. - */ - - public void enableEqualizer(boolean enable) { - if (enable != mEqualizer.getEnabled()) { - if (!enable) { - for (short i = 0; i < getNumEqualizerBands(); i++) { - mEqualizer.setBandLevel(i, (short) 0); - } - } - mEqualizer.setEnabled(enable); - } - } - - public void setEqualizerLevels(short[] levels) { - if (mEqualizer.getEnabled()) { - for (short i = 0; i < levels.length; i++) { - if (mEqualizer.getBandLevel(i) != levels[i]) { - mEqualizer.setBandLevel(i, levels[i]); - } - } - } - } - - public short getNumEqualizerBands() { - if (mEqNumBands < 0) { - mEqNumBands = mEqualizer.getNumberOfBands(); - } - return mEqNumBands; - } - - public short getNumEqualizerPresets() { - if (mEqNumPresets < 0) { - mEqNumPresets = mEqualizer.getNumberOfPresets(); - } - return mEqNumPresets; - } - - public void enableBassBoost(boolean enable) { - if (enable != mBassBoost.getEnabled()) { - if (!enable) { - mBassBoost.setStrength((short) 1); - mBassBoost.setStrength((short) 0); - } - mBassBoost.setEnabled(enable); - } - } - - public void setBassBoostStrength(short strength) { - if (mBassBoost.getEnabled() && mBassBoost.getRoundedStrength() != strength) { - mBassBoost.setStrength(strength); - } - } - - public void enableVirtualizer(boolean enable) { - if (enable != mVirtualizer.getEnabled()) { - if (!enable) { - mVirtualizer.setStrength((short) 1); - mVirtualizer.setStrength((short) 0); - } - mVirtualizer.setEnabled(enable); - } - } - - public void setVirtualizerStrength(short strength) { - if (mVirtualizer.getEnabled() && mVirtualizer.getRoundedStrength() != strength) { - mVirtualizer.setStrength(strength); - } - } - - public void enableReverb(boolean enable) { - if (enable != mPresetReverb.getEnabled()) { - if (!enable) { - mPresetReverb.setPreset((short) 0); - } - mPresetReverb.setEnabled(enable); - } - } - - public void setReverbPreset(short preset) { - if (mPresetReverb.getEnabled() && mPresetReverb.getPreset() != preset) { - mPresetReverb.setPreset(preset); - } - } - - public void release() { - mEqualizer.release(); - mBassBoost.release(); - mVirtualizer.release(); - mPresetReverb.release(); - } - } - - protected static final String TAG = HeadsetService.class.getSimpleName(); - public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - - private void addSession(int sessionId) { - if (sessionId == 0) { - return; - } - if (DEBUG) Log.i(TAG, String.format("New audio session: %d", sessionId)); - - synchronized (mAudioSessionsL) { - if (mAudioSessionsL.indexOfKey(sessionId) < 0) { - mAudioSessionsL.put(sessionId, new EffectSet(sessionId)); - } - updateLocked(); - } - } - - private void removeSession(int sessionId) { - if (sessionId == 0) { - return; - } - if (DEBUG) Log.i(TAG, String.format("Audio session removed: %d", sessionId)); - - synchronized (mAudioSessionsL) { - EffectSet gone = mAudioSessionsL.removeReturnOld(sessionId); - if (gone != null) { - gone.release(); - } - } - } - - public void addSession(AudioSessionInfo info) { - if (info.getStream() == AudioManager.STREAM_MUSIC && - (info.getFlags() < 0 || (info.getFlags() & 0x8) > 0 || (info.getFlags() & 0x10) > 0) && - (info.getChannelMask() < 0 || info.getChannelMask() > 1)) { - - // Never auto-attach is someone is recording! We don't want to - // interfere with any sort of - // loopback mechanisms. - final boolean recording = AudioSystem.isSourceActive(0) - || AudioSystem.isSourceActive(6); - if (recording) { - Log.w(TAG, "Recording in progress, not performing auto-attach!"); - return; - } - addSession(info.getSessionId()); - } - } - - public void removeSession(AudioSessionInfo info) { - if (info.getStream() == AudioManager.STREAM_MUSIC) { - removeSession(info.getSessionId()); - } - } - - public class LocalBinder extends Binder { - public HeadsetService getService() { - return HeadsetService.this; - } - } - - private final LocalBinder mBinder = new LocalBinder(); - - /** - * Known audio sessions and their associated audioeffect suites. - */ - private final SparseArray<EffectSet> mAudioSessionsL = new SparseArray<EffectSet>(); - - AudioPortListener mAudioPortListener; - - /** - * Has DSPManager assumed control of equalizer levels? - */ - private float[] mOverriddenEqualizerLevels; - - /** - * Update audio parameters when preferences have been updated. - */ - private final BroadcastReceiver mPreferenceUpdateReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.i(TAG, "Preferences updated."); - update(); - } - }; - - private class AudioPortListener implements AudioManager.OnAudioPortUpdateListener { - private boolean mUseBluetooth; - private boolean mUseHeadset; - private boolean mUseUSB; - private boolean mUseWifiDisplay; - private boolean mUseSpeaker; - private boolean mUseLineOut; - - private final Context mContext; - - public AudioPortListener(Context context) { - mContext = context; - } - - @Override - public void onAudioPortListUpdate(AudioPort[] portList) { - final boolean prevUseHeadset = mUseHeadset; - final boolean prevUseBluetooth = mUseBluetooth; - final boolean prevUseUSB = mUseUSB; - final boolean prevUseWireless = mUseWifiDisplay; - final boolean prevUseSpeaker = mUseSpeaker; - final boolean prevUseLineOut = mUseLineOut; - - AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - int device = am.getDevicesForStream(AudioManager.STREAM_MUSIC); - mUseBluetooth = (device & AudioManager.DEVICE_OUT_BLUETOOTH_A2DP) != 0 - || (device & AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES) != 0 - || (device & AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER) != 0 - || (device & AudioManager.DEVICE_OUT_BLUETOOTH_SCO) != 0 - || (device & AudioManager.DEVICE_OUT_BLUETOOTH_SCO_CARKIT) != 0 - || (device & AudioManager.DEVICE_OUT_BLUETOOTH_SCO_HEADSET) != 0; - - mUseHeadset = (device & AudioManager.DEVICE_OUT_WIRED_HEADPHONE) != 0 - || (device & AudioManager.DEVICE_OUT_WIRED_HEADSET) != 0; - - mUseLineOut = (device & AudioManager.DEVICE_OUT_LINE) != 0; - - mUseUSB = (device & AudioManager.DEVICE_OUT_USB_ACCESSORY) != 0 - || (device & AudioManager.DEVICE_OUT_USB_DEVICE) != 0; - - mUseWifiDisplay = false; //TODO add support for wireless display.. - - mUseSpeaker = (device & AudioManager.DEVICE_OUT_SPEAKER) != 0; - - Log.i(TAG, "Headset=" + mUseHeadset + "; Bluetooth=" - + mUseBluetooth + " ; USB=" + mUseUSB + "; Speaker=" + mUseSpeaker + - "; Line out=" + mUseLineOut); - - if (prevUseHeadset != mUseHeadset - || prevUseBluetooth != mUseBluetooth - || prevUseUSB != mUseUSB - || prevUseWireless != mUseWifiDisplay - || prevUseSpeaker != mUseSpeaker - || prevUseLineOut != mUseLineOut) { - - update(); - - Intent i = new Intent(ACTION_UPDATE_PREFERENCES); - mContext.sendBroadcast(i); - } - } - - @Override - public void onAudioPatchListUpdate(AudioPatch[] patchList) { - - } - - @Override - public void onServiceDied() { - - } - - public String getInternalAudioOutputRouting() { - if (mUseSpeaker) { - return "speaker"; - } - if (mUseBluetooth) { - return "bluetooth"; - } - if (mUseHeadset) { - return "headset"; - } - if (mUseUSB) { - return "usb"; - } - if (mUseWifiDisplay) { - return "wireless"; - } - if (mUseLineOut) { - return "lineout"; - } - return "speaker"; - } - } - - @Override - public void onCreate() { - super.onCreate(); - Log.i(TAG, "Starting service."); - - registerReceiver(mPreferenceUpdateReceiver, - new IntentFilter(ACTION_UPDATE_PREFERENCES)); - - AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - am.registerAudioPortUpdateListener(mAudioPortListener = new AudioPortListener(this)); - - saveDefaults(); - } - - - /** - * maps {@link AudioEffect#EXTRA_CONTENT_TYPE} to an AudioManager.STREAM_* item - */ - private static int mapContentTypeToStream(int contentType) { - switch (contentType) { - case AudioEffect.CONTENT_TYPE_VOICE: - return AudioManager.STREAM_VOICE_CALL; - case AudioEffect.CONTENT_TYPE_GAME: - // explicitly don't support game effects right now - return -1; - case AudioEffect.CONTENT_TYPE_MOVIE: - case AudioEffect.CONTENT_TYPE_MUSIC: - default: - return AudioManager.STREAM_MUSIC; - } - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null && intent.getAction() != null) { - if (DEBUG) { - Log.i(TAG, "onStartCommand() called with " + "intent = [" + intent + "], flags = [" - + flags + "], startId = [" + startId + "], extras = [" + - (intent.getExtras() == null ? "null" : intent.getExtras().toString()) - + "]"); - } - String action = intent.getAction(); - int sessionId = intent.getIntExtra(AudioEffect.EXTRA_AUDIO_SESSION, 0); - String pkg = intent.getStringExtra(AudioEffect.EXTRA_PACKAGE_NAME); - int stream = mapContentTypeToStream( - intent.getIntExtra(AudioEffect.EXTRA_CONTENT_TYPE, - AudioEffect.CONTENT_TYPE_MUSIC)); - - if (action.equals(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)) { - if (DEBUG) { - Log.i(TAG, String.format("New audio session: %d package: %s contentType=%d", - sessionId, pkg, stream)); - } - addSession(sessionId); - - } else if (action.equals(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)) { - - removeSession(sessionId); - - } else if (action.equals(CMAudioManager.ACTION_AUDIO_SESSIONS_CHANGED)) { - - final AudioSessionInfo info = (AudioSessionInfo) intent.getParcelableExtra( - CMAudioManager.EXTRA_SESSION_INFO); - if (info != null && info.getSessionId() > 0) { - boolean added = intent.getBooleanExtra(CMAudioManager.EXTRA_SESSION_ADDED, - false); - if (added) { - addSession(info); - } else { - removeSession(info); - } - } - } - } - return START_STICKY; - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.i(TAG, "Stopping service."); - - unregisterReceiver(mPreferenceUpdateReceiver); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - /** - * Gain temporary control over the global equalizer. - * Used by DSPManager when testing a new equalizer setting. - * - * @param levels - */ - public void setEqualizerLevels(float[] levels) { - mOverriddenEqualizerLevels = levels; - update(); - } - - /** - * There appears to be no way to find out what the current actual audio routing is. - * For instance, if a wired headset is plugged in, the following objects/classes are involved: - * </p> - * <ol> - * <li>wiredaccessoryobserver</li> - * <li>audioservice</li> - * <li>audiosystem</li> - * <li>audiopolicyservice</li> - * <li>audiopolicymanager</li> - * </ol> - * <p>Once the decision of new routing has been made by the policy manager, it is relayed to - * audiopolicyservice, which waits for some time to let application buffers drain, and then - * informs it to hardware. The full chain is:</p> - * <ol> - * <li>audiopolicymanager</li> - * <li>audiopolicyservice</li> - * <li>audiosystem</li> - * <li>audioflinger</li> - * <li>audioeffect (if any)</li> - * </ol> - * <p>However, the decision does not appear to be relayed to java layer, so we must - * make a guess about what the audio output routing is.</p> - * - * @return string token that identifies configuration to use - */ - public String getAudioOutputRouting() { - if (mAudioPortListener != null) { - return mAudioPortListener.getInternalAudioOutputRouting(); - } - return "speaker"; - } - - public EffectSet getEffects(int session) { - synchronized (mAudioSessionsL) { - return mAudioSessionsL.get(session); - } - } - - private void saveDefaults() { - EffectSet temp; - try { - temp = new EffectSet(0); - } catch (Exception e) { - // this is really bad- likely the media stack is broken. - // disable ourself if we get into this state, as the service - // will restart itself repeatedly! - Log.e(TAG, e.getMessage(), e); - stopSelf(); - return; - } - - SharedPreferences prefs = getSharedPreferences("global", 0); - - final int numBands = temp.getNumEqualizerBands(); - final int numPresets = temp.getNumEqualizerPresets(); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString("equalizer.number_of_presets", String.valueOf(numPresets)).apply(); - editor.putString("equalizer.number_of_bands", String.valueOf(numBands)).apply(); - - // range - short[] rangeShortArr = temp.mEqualizer.getBandLevelRange(); - - - editor.putString("equalizer.band_level_range", rangeShortArr[0] + ";" + rangeShortArr[1]).apply(); - - // center freqs - StringBuilder centerFreqs = new StringBuilder(); - // audiofx.global.centerfreqs - for (short i = 0; i < numBands; i++) { - centerFreqs.append(temp.mEqualizer.getCenterFreq(i)); - centerFreqs.append(";"); - - } - centerFreqs.deleteCharAt(centerFreqs.length() - 1); - editor.putString("equalizer.center_freqs", centerFreqs.toString()).apply(); - - // populate preset names - StringBuilder presetNames = new StringBuilder(); - for (int i = 0; i < numPresets; i++) { - String presetName = temp.mEqualizer.getPresetName((short) i); - presetNames.append(presetName); - presetNames.append("|"); - - // populate preset band values - StringBuilder presetBands = new StringBuilder(); - temp.mEqualizer.usePreset((short) i); - - for (int j = 0; j < numBands; j++) { - // loop through preset bands - presetBands.append(temp.mEqualizer.getBandLevel((short) j)); - presetBands.append(";"); - } - presetBands.deleteCharAt(presetBands.length() - 1); - editor.putString("equalizer.preset." + i, presetBands.toString()).apply(); - } - presetNames.deleteCharAt(presetNames.length() - 1); - editor.putString("equalizer.preset_names", presetNames.toString()).apply(); - temp.release(); - - // add ci-extreme - StringBuilder ciExtremeBuilder = new StringBuilder("0;800;400;100;1000"); - if (numBands > 5) { - int extraBands = numBands - 5; - for (int i = 0; i < extraBands; i++) { - ciExtremeBuilder.insert(0, "0;"); - } - } - editor.putString("equalizer.preset." + numPresets, ciExtremeBuilder.toString()).apply(); - - // add small-speaker - StringBuilder ssBuilder = new StringBuilder("-170;270;50;-220;200"); - if (numBands > 5) { - int extraBands = numBands - 5; - for (int i = 0; i < extraBands; i++) { - ssBuilder.insert(0, "0;"); - } - } - editor.putString("equalizer.preset." + (numPresets + 1), ssBuilder.toString()).apply(); - editor.commit(); - - // Enable for the speaker by default - if (!getSharedPrefsFile("speaker").exists()) { - SharedPreferences spk = getSharedPreferences("speaker", 0); - spk.edit().putBoolean("audiofx.global.enable", true).apply(); - spk.edit().putString("audiofx.eq.preset", String.valueOf(numPresets + 1)).apply(); - } - } - - /** - * Push new configuration to audio stack. - */ - void update() { - synchronized (mAudioSessionsL) { - updateLocked(); - } - } - - private void updateLocked() { - final String mode = getAudioOutputRouting(); - SharedPreferences preferences = getSharedPreferences( - mode, 0); - - if (DEBUG) Log.i(TAG, "Selected configuration: " + mode); - - for (int i = 0; i < mAudioSessionsL.size(); i++) { - updateDsp(preferences, mAudioSessionsL.valueAt(i)); - } - } - - private void updateDsp(SharedPreferences prefs, EffectSet session) { - final boolean globalEnabled = prefs.getBoolean("audiofx.global.enable", false); - - try { - session.enableBassBoost(globalEnabled && prefs.getBoolean("audiofx.bass.enable", false)); - session.setBassBoostStrength(Short.valueOf(prefs - .getString("audiofx.bass.strength", "0"))); - - } catch (Exception e) { - Log.e(TAG, "Error enabling bass boost!", e); - } - - try { - short preset = Short.decode(prefs.getString("audiofx.reverb.preset", - String.valueOf(PresetReverb.PRESET_NONE))); - session.enableReverb(globalEnabled && (preset > 0)); - session.setReverbPreset(preset); - - } catch (Exception e) { - Log.e(TAG, "Error enabling reverb preset", e); - } - - try { - session.enableEqualizer(globalEnabled); - final int customPresetPos = session.getNumEqualizerPresets() + 2; - final int preset = Integer.valueOf(prefs.getString("audiofx.eq.preset", - String.valueOf(customPresetPos))); - final int bands = session.getNumEqualizerBands(); - - /* - * Equalizer state is in a single string preference with all values - * separated by ; - */ - String[] levels = null; - short[] equalizerLevels = null; - - if (mOverriddenEqualizerLevels != null) { - - } else if (preset == customPresetPos) { - if (DEBUG) Log.i(TAG, "loading custom band levels"); - levels = prefs.getString("audiofx.eq.bandlevels.custom", - getZeroedBandsString(bands)).split(";"); - } else { - if (DEBUG) Log.i(TAG, "loading preset band levels"); - levels = getSharedPreferences("global", 0).getString("equalizer.preset." + preset, - getZeroedBandsString(bands)).split(";"); - } - - if (levels != null) { - if (DEBUG) Log.i(TAG, "band levels applied: " + Arrays.toString(levels)); - equalizerLevels = new short[levels.length]; - for (int i = 0; i < levels.length; i++) { - equalizerLevels[i] = (short) (Float.parseFloat(levels[i])); - } - } else if (mOverriddenEqualizerLevels != null) { - equalizerLevels = new short[mOverriddenEqualizerLevels.length]; - for (int i = 0; i < mOverriddenEqualizerLevels.length; i++) { - equalizerLevels[i] = (short) mOverriddenEqualizerLevels[i]; - } - } - if (equalizerLevels != null) { - session.setEqualizerLevels(equalizerLevels); - } - - - } catch (Exception e) { - Log.e(TAG, "Error enabling equalizer!", e); - } - - try { - session.enableVirtualizer(globalEnabled - && prefs.getBoolean("audiofx.virtualizer.enable", false)); - session.setVirtualizerStrength(Short.valueOf(prefs.getString( - "audiofx.virtualizer.strength", "0"))); - - } catch (Exception e) { - Log.e(TAG, "Error enabling virtualizer!"); - } - } -} diff --git a/src/org/cyanogenmod/audiofx/OpenSLESConstants.java b/src/org/cyanogenmod/audiofx/OpenSLESConstants.java deleted file mode 100644 index 2131b55..0000000 --- a/src/org/cyanogenmod/audiofx/OpenSLESConstants.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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/audiofx/AudioFxApplication.java b/src/org/cyanogenmod/audiofx/audiofx/AudioFxApplication.java new file mode 100644 index 0000000..fa47637 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/AudioFxApplication.java @@ -0,0 +1,47 @@ +/* + * 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. + */ +package com.cyngn.audiofx; + +import android.app.Application; +import android.util.Log; + +import com.cyanogen.ambient.analytics.AnalyticsServices; +import com.cyanogen.ambient.analytics.Event; +import com.cyanogen.ambient.common.api.AmbientApiClient; + +public class AudioFxApplication extends Application { + + private static final String TAG = AudioFxApplication.class.getSimpleName(); + private static final boolean DEBUG = false; + + private AmbientApiClient mClient; + + @Override + public void onCreate() { + super.onCreate(); + mClient = new AmbientApiClient.Builder(this) + .addApi(AnalyticsServices.API) + .build(); + mClient.connect(); + } + + public void sendEvent(Event event) { + if (DEBUG) { + Log.i(TAG, "sendEvent() called with event = [" + event + "]"); + } + AnalyticsServices.AnalyticsApi.sendEvent(mClient, event); + } +} diff --git a/src/org/cyanogenmod/audiofx/Compatibility.java b/src/org/cyanogenmod/audiofx/audiofx/Compatibility.java index c0e72c2..313c480 100644 --- a/src/org/cyanogenmod/audiofx/Compatibility.java +++ b/src/org/cyanogenmod/audiofx/audiofx/Compatibility.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.cyanogenmod.audiofx; +package com.cyngn.audiofx; import android.app.Activity; import android.app.IntentService; @@ -30,6 +30,8 @@ import android.media.audiofx.AudioEffect; import android.net.Uri; import android.os.Bundle; import android.util.Log; +import com.cyngn.audiofx.activity.ActivityMusic; +import com.cyngn.audiofx.stats.UserSession; import java.util.List; @@ -64,8 +66,8 @@ public class Compatibility { 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); + String defPackage = pref.getString(Constants.MUSICFX_DEFAULT_PACKAGE_KEY, null); + String defName = pref.getString(Constants.MUSICFX_DEFAULT_PANEL_KEY, null); log("read " + defPackage + "/" + defName + " as default"); if (defPackage == null || defName == null) { Log.e(TAG, "no default set!"); @@ -79,6 +81,8 @@ public class Compatibility { } else { i.setComponent(new ComponentName(defPackage, defName)); } + + i.putExtra(ActivityMusic.EXTRA_CALLING_PACKAGE, getCallingPackage()); startActivity(i); finish(); } @@ -149,9 +153,9 @@ public class Compatibility { 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); + SharedPreferences pref = Constants.getMusicFxPrefs(this); + String savedDefPackage = pref.getString(Constants.MUSICFX_DEFAULT_PACKAGE_KEY, null); + String savedDefName = pref.getString(Constants.MUSICFX_DEFAULT_PANEL_KEY, null); log("saved default: " + savedDefName); for (ResolveInfo foo: ris) { if (foo.activityInfo.name.equals(Compatibility.Redirector.class.getName())) { @@ -206,10 +210,10 @@ public class Compatibility { // Write the selected default to the prefs so that the Redirector activity // knows which one to use. - SharedPreferences pref = getSharedPreferences("musicfx", MODE_PRIVATE); + SharedPreferences pref = Constants.getMusicFxPrefs(this); Editor ed = pref.edit(); - ed.putString("defaultpanelpackage", defPackage); - ed.putString("defaultpanelname", defName); + ed.putString(Constants.MUSICFX_DEFAULT_PACKAGE_KEY, defPackage); + ed.putString(Constants.MUSICFX_DEFAULT_PANEL_KEY, defName); ed.commit(); log("wrote " + defPackage + "/" + defName + " as default"); } diff --git a/src/org/cyanogenmod/audiofx/audiofx/Constants.java b/src/org/cyanogenmod/audiofx/audiofx/Constants.java new file mode 100644 index 0000000..c1d5475 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/Constants.java @@ -0,0 +1,160 @@ +/* + * 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. + */ +package com.cyngn.audiofx; + +import android.content.Context; +import android.content.SharedPreferences; +import com.cyngn.audiofx.eq.EqUtils; + +import java.util.ArrayList; +import java.util.List; + +public class Constants { + + // current pref version, bump to rebuild prefs + public static final int CURRENT_PREFS_INT_VERSION = 2; + + // effect type identifiers + public static final int EFFECT_TYPE_ANDROID = 1; + public static final int EFFECT_TYPE_MAXXAUDIO = 2; + public static final int EFFECT_TYPE_DTS = 3; + + // global settings + public static final String AUDIOFX_GLOBAL_FILE = "global"; + + public static final String DEVICE_SPEAKER = "speaker"; + public static final String DEVICE_HEADSET = "headset"; + public static final String DEVICE_LINE_OUT = "lineout"; + public static final String DEVICE_PREFIX_USB = "usb"; + public static final String DEVICE_PREFIX_CAST = "wireless"; + public static final String DEVICE_PREFIX_BLUETOOTH = "bluetooth"; + + public static final String SAVED_DEFAULTS = "saved_defaults"; + + public static final String AUDIOFX_GLOBAL_USE_DTS = "audiofx.global.use_dts"; + public static final String AUDIOFX_GLOBAL_HAS_DTS = "audiofx.global.has_dts"; + public static final String AUDIOFX_GLOBAL_ENABLE_DTS = "audiofx.global.dts.enable"; + public static final String AUDIOFX_GLOBAL_HAS_MAXXAUDIO = "audiofx.global.hasmaxxaudio"; + public static final String AUDIOFX_GLOBAL_HAS_BASSBOOST = "audiofx.global.hasbassboost"; + public static final String AUDIOFX_GLOBAL_HAS_VIRTUALIZER = "audiofx.global.hasvirtualizer"; + public static final String AUDIOFX_GLOBAL_PREFS_VERSION_INT = "audiofx.global.prefs.version"; + + // per-device settings + public static final boolean DEVICE_DEFAULT_GLOBAL_ENABLE = false; + + /** + * not really global enable, but really the device global enable... + */ + public static final String DEVICE_AUDIOFX_GLOBAL_ENABLE = "audiofx.global.enable"; + public static final String DEVICE_AUDIOFX_BASS_ENABLE = "audiofx.bass.enable"; + public static final String DEVICE_AUDIOFX_BASS_STRENGTH = "audiofx.bass.strength"; + public static final String DEVICE_AUDIOFX_REVERB_PRESET = "audiofx.reverb.preset"; + public static final String DEVICE_AUDIOFX_VIRTUALIZER_ENABLE = "audiofx.virtualizer.enable"; + public static final String DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH = "audiofx.virtualizer.strength"; + public static final String DEVICE_AUDIOFX_TREBLE_ENABLE = "audiofx.treble.enable"; + public static final String DEVICE_AUDIOFX_TREBLE_STRENGTH = "audiofx.treble.strength"; + public static final String DEVICE_AUDIOFX_MAXXVOLUME_ENABLE = "audiofx.maxxvolume.enable"; + + public static final String DEVICE_AUDIOFX_EQ_PRESET = "audiofx.eq.preset"; + public static final String DEVICE_AUDIOFX_EQ_PRESET_LEVELS = "audiofx.eq.preset.levels"; + + // eq + public static final String EQUALIZER_NUMBER_OF_PRESETS = "equalizer.number_of_presets"; + public static final String EQUALIZER_NUMBER_OF_BANDS = "equalizer.number_of_bands"; + public static final String EQUALIZER_BAND_LEVEL_RANGE = "equalizer.band_level_range"; + public static final String EQUALIZER_CENTER_FREQS = "equalizer.center_freqs"; + public static final String EQUALIZER_PRESET = "equalizer.preset."; + public static final String EQUALIZER_PRESET_NAMES = "equalizer.preset_names"; + + // musicfx constants + public static final String MUSICFX_PREF_NAME = "musicfx"; + public static final String MUSICFX_DEFAULT_PACKAGE_KEY = "defaultpanelpackage"; + public static final String MUSICFX_DEFAULT_PANEL_KEY = "defaultpanelname"; + + public static SharedPreferences getMusicFxPrefs(Context context) { + return context.getSharedPreferences(MUSICFX_PREF_NAME, Context.MODE_PRIVATE); + } + + public static SharedPreferences getGlobalPrefs(Context context) { + return context.getSharedPreferences(AUDIOFX_GLOBAL_FILE, 0); + } + + public static List<Preset> getCustomPresets(Context ctx, int bands) { + ArrayList<Preset> presets = new ArrayList<Preset>(); + final SharedPreferences presetPrefs = ctx.getSharedPreferences("custom_presets", 0); + String[] presetNames = presetPrefs.getString("preset_names", "").split("\\|"); + + for (int i = 0; i < presetNames.length; i++) { + String storedPresetString = presetPrefs.getString(presetNames[i], null); + if (storedPresetString == null) { + continue; + } + Preset.CustomPreset p = Preset.CustomPreset.fromString(storedPresetString); + presets.add(p); + } + + return presets; + } + + public static void saveCustomPresets(Context ctx, List<Preset> presets) { + final SharedPreferences.Editor presetPrefs = ctx.getSharedPreferences("custom_presets", 0).edit(); + presetPrefs.clear(); + + StringBuffer presetNames = new StringBuffer(); + for (int i = 0; i < presets.size(); i++) { + final Preset preset = presets.get(i); + if (preset instanceof Preset.CustomPreset + && !(preset instanceof Preset.PermCustomPreset)) { + Preset.CustomPreset p = (Preset.CustomPreset) preset; + presetNames.append(p.getName()); + presetNames.append("|"); + + presetPrefs.putString(p.getName(), p.toString()); + } + } + if (presetNames.length() > 0) { + presetNames.deleteCharAt(presetNames.length() - 1); + } + + presetPrefs.putString("preset_names", presetNames.toString()); + presetPrefs.commit(); + } + + public static int[] getBandLevelRange(Context context) { + String savedCenterFreqs = context.getSharedPreferences("global", 0).getString("equalizer.band_level_range", null); + if (savedCenterFreqs == null || savedCenterFreqs.isEmpty()) { + return new int[]{-1500, 1500}; + } else { + String[] split = savedCenterFreqs.split(";"); + int[] freqs = new int[split.length]; + for (int i = 0; i < split.length; i++) { + freqs[i] = Integer.valueOf(split[i]); + } + return freqs; + } + } + + public static int[] getCenterFreqs(Context context, int eqBands) { + String savedCenterFreqs = context.getSharedPreferences("global", 0).getString("equalizer.center_freqs", + EqUtils.getZeroedBandsString(eqBands)); + String[] split = savedCenterFreqs.split(";"); + int[] freqs = new int[split.length]; + for (int i = 0; i < split.length; i++) { + freqs[i] = Integer.valueOf(split[i]); + } + return freqs; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/Preset.java b/src/org/cyanogenmod/audiofx/audiofx/Preset.java new file mode 100644 index 0000000..e284658 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/Preset.java @@ -0,0 +1,236 @@ +package com.cyngn.audiofx; + +import android.os.Parcel; +import android.os.Parcelable; +import com.cyngn.audiofx.eq.EqUtils; + +public class Preset implements Parcelable { + + protected String mName; + protected final float[] mLevels; + + private Preset(String name, float[] levels) { + this.mName = name; + mLevels = new float[levels.length]; + for (int i = 0; i < levels.length; i++) { + mLevels[i] = levels[i]; + } + } + + public float[] getLevels() { + return mLevels; + } + + public float getBandLevel(int band) { + return mLevels[band]; + } + + @Override + public String toString() { + return mName + "|" + EqUtils.floatLevelsToString(mLevels); + } + + private static Preset fromString(String input) { + final String[] split = input.split("\\|"); + if (split == null || split.length != 2) { + return null; + } + float[] levels = EqUtils.stringBandsToFloats(split[1]); + return new Preset(split[0], levels); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Preset) { + Preset other = (Preset) o; + + if (this.mLevels.length != ((Preset) o).mLevels.length) { + return false; + } + + for(int i = 0; i < mLevels.length; i++) { + if (mLevels[i] != other.mLevels[i]) { + return false; + } + } + + return other.mName.equals(mName); + } + return super.equals(o); + } + + private Preset(Parcel in) { + if (in.readInt() == 1) { + mName = in.readString(); + } + mLevels = new float[in.readInt()]; + in.readFloatArray(mLevels); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mName != null ? 1 : 0); + if (mName != null) { + dest.writeString(mName); + } + dest.writeInt(mLevels.length); + dest.writeFloatArray(mLevels); + } + + public static final Parcelable.Creator<Preset> CREATOR = new Parcelable.Creator<Preset>() { + @Override + public Preset createFromParcel(Parcel in) { + return new Preset(in); + } + + @Override + public Preset[] newArray(int size) { + return new Preset[size]; + } + }; + + public String getName() { + return mName; + } + + public static class StaticPreset extends Preset { + public StaticPreset(String name, float[] levels) { + super(name, levels); + } + } + + public static class CustomPreset extends Preset { + + private boolean mLocked; + + public CustomPreset(String name, float[] levels, boolean locked) { + super(name, levels); + mLocked = locked; + } + + public boolean isLocked() { + return mLocked; + } + + public void setLocked(boolean locked) { + mLocked = locked; + } + + public void setName(String name) { + mName = name; + } + + public void setLevel(int band, float level) { + mLevels[band] = level; + } + + public void setLevels(float[] levels) { + for (int i = 0; i < levels.length; i++) { + mLevels[i] = levels[i]; + } + } + + public float getLevel(int band) { + return mLevels[band]; + } + + @Override + public boolean equals(Object o) { + if (o instanceof CustomPreset) { + return super.equals(o) && mLocked == ((CustomPreset) o).mLocked; + } + return false; + } + + @Override + public String toString() { + return super.toString() + "|" + mLocked; + } + + public static CustomPreset fromString(String input) { + final String[] split = input.split("\\|"); + if (split == null || split.length != 3) { + return null; + } + float[] levels = EqUtils.stringBandsToFloats(split[1]); + return new CustomPreset(split[0], levels, Boolean.valueOf(split[2])); + } + + public static final Parcelable.Creator<CustomPreset> CREATOR + = new Parcelable.Creator<CustomPreset>() { + @Override + public CustomPreset createFromParcel(Parcel in) { + return new CustomPreset(in); + } + + @Override + public CustomPreset[] newArray(int size) { + return new CustomPreset[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mLocked ? 1 : 0); + } + + protected CustomPreset(Parcel in) { + super(in); + mLocked = in.readInt() == 1; + } + + } + + public static class PermCustomPreset extends CustomPreset { + + public PermCustomPreset(String name, float[] levels) { + super(name, levels, false); + } + + @Override + public String toString() { + return mName + "|" + EqUtils.floatLevelsToString(mLevels); + } + + public static PermCustomPreset fromString(String input) { + final String[] split = input.split("\\|"); + if (split == null || split.length != 2) { + return null; + } + float[] levels = EqUtils.stringBandsToFloats(split[1]); + return new PermCustomPreset(split[0], levels); + } + + protected PermCustomPreset(Parcel in) { + super(in); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + } + + public static final Creator<PermCustomPreset> CREATOR = new Creator<PermCustomPreset>() { + @Override + public PermCustomPreset createFromParcel(Parcel in) { + return new PermCustomPreset(in); + } + + @Override + public PermCustomPreset[] newArray(int size) { + return new PermCustomPreset[size]; + } + }; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/activity/ActivityMusic.java b/src/org/cyanogenmod/audiofx/audiofx/activity/ActivityMusic.java new file mode 100644 index 0000000..f75273d --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/activity/ActivityMusic.java @@ -0,0 +1,198 @@ +package com.cyngn.audiofx.activity; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Bundle; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewStub; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import com.cyanogen.ambient.analytics.Event; +import com.cyngn.audiofx.AudioFxApplication; +import com.cyngn.audiofx.Constants; +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.fragment.AudioFxFragment; +import com.cyngn.audiofx.knobs.KnobCommander; +import com.cyngn.audiofx.service.AudioFxService; +import com.cyngn.audiofx.stats.AppState; +import com.cyngn.audiofx.stats.UserSession; + +public class ActivityMusic extends Activity { + + private static final String TAG = ActivityMusic.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + public static final String TAG_AUDIOFX = "audiofx"; + public static final String EXTRA_CALLING_PACKAGE = "audiofx::extra_calling_package"; + + private CheckBox mCurrentDeviceToggle; + MasterConfigControl mConfig; + String mCallingPackage; + + private boolean mWaitingForService = true; + private SharedPreferences.OnSharedPreferenceChangeListener mServiceReadyObserver; + + private CompoundButton.OnCheckedChangeListener mGlobalEnableToggleListener + = new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(final CompoundButton buttonView, + final boolean isChecked) { + if (UserSession.getInstance() != null) { + UserSession.getInstance().deviceEnabledDisabled(); + } + mConfig.setCurrentDeviceEnabled(isChecked); + } + }; + + @Override + public void onCreate(final Bundle savedInstanceState) { + if (DEBUG) + Log.i(TAG, "onCreate() called with " + + "savedInstanceState = [" + savedInstanceState + "]"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mCallingPackage = getIntent().getStringExtra(EXTRA_CALLING_PACKAGE); + Log.i(TAG, "calling package: " + mCallingPackage); + + mConfig = MasterConfigControl.getInstance(this); + + final SharedPreferences globalPrefs = Constants.getGlobalPrefs(this); + + mWaitingForService = !defaultsSetup(); + if (mWaitingForService) { + Log.w(TAG, "waiting for service."); + mServiceReadyObserver = new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + if (key.equals(Constants.SAVED_DEFAULTS) && defaultsSetup()) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); + mConfig.onResetDefaults(); + init(savedInstanceState); + + mWaitingForService = false; + invalidateOptionsMenu(); + mServiceReadyObserver = null; + } + } + }; + globalPrefs.registerOnSharedPreferenceChangeListener(mServiceReadyObserver); + startService(new Intent(ActivityMusic.this, AudioFxService.class)); + // TODO add loading fragment if service initialization takes too long + } else { + init(savedInstanceState); + } + } + + private boolean defaultsSetup() { + final int targetVersion = Constants.CURRENT_PREFS_INT_VERSION; + final SharedPreferences prefs = Constants.getGlobalPrefs(this); + final int currentVersion = prefs.getInt(Constants.AUDIOFX_GLOBAL_PREFS_VERSION_INT, 0); + final boolean defaultsSaved = prefs.getBoolean(Constants.SAVED_DEFAULTS, false); + return defaultsSaved && currentVersion >= targetVersion; + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + // should null it out if one was there, compat redirector with package will go through onCreate + mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE); + } + + @Override + protected void onDestroy() { + if (mServiceReadyObserver != null) { + Constants.getGlobalPrefs(this) + .unregisterOnSharedPreferenceChangeListener(mServiceReadyObserver); + mServiceReadyObserver = null; + } + super.onDestroy(); + } + + private void init(Bundle savedInstanceState) { + mConfig = MasterConfigControl.getInstance(this); + + ActionBar ab = getActionBar(); + ab.setTitle(R.string.app_title); + ab.setDisplayShowTitleEnabled(true); + + final View extraView = LayoutInflater.from(this) + .inflate(R.layout.action_bar_custom_components, null); + ActionBar.LayoutParams lp = new ActionBar.LayoutParams(ActionBar.LayoutParams.WRAP_CONTENT, + ActionBar.LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.CENTER_VERTICAL); + ab.setCustomView(extraView, lp); + ab.setDisplayShowCustomEnabled(true); + + mCurrentDeviceToggle = (CheckBox) ab.getCustomView().findViewById(R.id.global_toggle); + mCurrentDeviceToggle.setOnCheckedChangeListener(mGlobalEnableToggleListener); + + if (savedInstanceState == null && findViewById(R.id.main_fragment) != null) { + getFragmentManager() + .beginTransaction() + .add(R.id.main_fragment, new AudioFxFragment(), TAG_AUDIOFX) + .commit(); + } + applyOemDecor(); + } + + private void applyOemDecor() { + ActionBar ab = getActionBar(); + if (mConfig.hasMaxxAudio()) { + ab.setSubtitle(R.string.powered_by_maxx_audio); + } else if (mConfig.hasDts()) { + final ViewStub stub = (ViewStub) ab.getCustomView().findViewById(R.id.logo_stub); + stub.setLayoutResource(R.layout.action_bar_dts_logo); + stub.inflate(); + } + } + + @Override + protected void onResume() { + if (DEBUG) Log.i(TAG, "onResume() called with " + ""); + super.onResume(); + + // initiate a new session + new UserSession(mCallingPackage); + } + + @Override + protected void onPause() { + super.onPause(); + + if (DEBUG) Log.d(TAG, "Session: " + UserSession.getInstance()); + + final Event.Builder builder = new Event.Builder("session", "ended"); + UserSession.getInstance().append(builder); + AppState.appendState(mConfig, KnobCommander.getInstance(this), builder); + ((AudioFxApplication) getApplicationContext()).sendEvent(builder.build()); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (DEBUG) Log.i(TAG, "onConfigurationChanged() called with " + + "newConfig = [" + newConfig + "]"); + if (newConfig.orientation != getResources().getConfiguration().orientation) { + mCurrentDeviceToggle = null; + } + } + + public void setGlobalToggleChecked(boolean checked) { + if (mCurrentDeviceToggle != null) { + mCurrentDeviceToggle.setOnCheckedChangeListener(null); + mCurrentDeviceToggle.setChecked(checked); + mCurrentDeviceToggle.setOnCheckedChangeListener(mGlobalEnableToggleListener); + } + } + + public CompoundButton getGlobalSwitch() { + return mCurrentDeviceToggle; + } +} diff --git a/src/org/cyanogenmod/audiofx/ControlPanelPicker.java b/src/org/cyanogenmod/audiofx/audiofx/activity/ControlPanelPicker.java index 1c6eaaf..a9fd876 100644 --- a/src/org/cyanogenmod/audiofx/ControlPanelPicker.java +++ b/src/org/cyanogenmod/audiofx/audiofx/activity/ControlPanelPicker.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package org.cyanogenmod.audiofx; +package com.cyngn.audiofx.activity; 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 com.cyngn.audiofx.Compatibility; +import com.cyngn.audiofx.Compatibility.Service; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -31,11 +32,8 @@ 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 com.cyngn.audiofx.R; import java.util.List; @@ -81,9 +79,9 @@ public class ControlPanelPicker extends AlertActivity implements OnClickListener p.mOnClickListener = mItemClickListener; p.mLabelColumn = "title"; p.mIsSingleChoice = true; - p.mPositiveButtonText = getString(com.android.internal.R.string.ok); + p.mPositiveButtonText = getString(getOkStringResId()); p.mPositiveButtonListener = this; - p.mNegativeButtonText = getString(com.android.internal.R.string.cancel); + p.mNegativeButtonText = getString(getCancelStringResId()); p.mOnPrepareListViewListener = this; p.mTitle = getString(R.string.picker_title); p.mCheckedItem = defpanelidx; @@ -91,6 +89,14 @@ public class ControlPanelPicker extends AlertActivity implements OnClickListener setupAlert(); } + private int getOkStringResId() { + return getResources().getIdentifier("ok", "string", "android"); + } + + private int getCancelStringResId() { + return getResources().getIdentifier("cancel", "string", "android"); + } + private DialogInterface.OnClickListener mItemClickListener = new DialogInterface.OnClickListener() { diff --git a/src/org/cyanogenmod/audiofx/audiofx/activity/EqualizerManager.java b/src/org/cyanogenmod/audiofx/audiofx/activity/EqualizerManager.java new file mode 100644 index 0000000..03e9853 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/activity/EqualizerManager.java @@ -0,0 +1,634 @@ +package com.cyngn.audiofx.activity; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.widget.CompoundButton; + +import com.cyngn.audiofx.Constants; +import com.cyngn.audiofx.Preset; +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.eq.EqUtils; +import com.cyngn.audiofx.service.AudioFxService; +import com.cyngn.audiofx.stats.UserSession; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +public class EqualizerManager { + + private static final String TAG = EqualizerManager.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final MasterConfigControl mConfig; + private final Context mContext; + + private float mMinFreq; + private float mMaxFreq; + + private float mMinDB; + private float mMaxDB; + private int mNumBands; + private CompoundButton.OnCheckedChangeListener mLockChangeListener; + + /* + * presets from the library custom preset. + */ + private int mPredefinedPresets; + private float[] mCenterFreqs; + private float[] mGlobalLevels; + + private AtomicBoolean mAnimatingToCustom = new AtomicBoolean(false); + + // whether we are in between presets, animating them and such + private boolean mChangingPreset = false; + + private int mCurrentPreset; + + private final ArrayList<Preset> mEqPresets = new ArrayList<Preset>(); + private int mEQCustomPresetPosition; + + private String mZeroedBandString; + + private static final int MSG_SAVE_PRESETS = 1; + private static final int MSG_SEND_EQ_OVERRIDE = 2; + + private Handler mHandler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_SAVE_PRESETS: + Constants.saveCustomPresets(mContext, mEqPresets); + break; + case MSG_SEND_EQ_OVERRIDE: + mConfig.overrideEqLevels((short)msg.arg1, (short) msg.arg2); + break; + } + return true; + }}, true); + + public EqualizerManager(Context context, MasterConfigControl config) { + mContext = context; + mConfig = config; + + applyDefaults(); + } + + public void applyDefaults() { + mEqPresets.clear(); + // setup eq + int bands = Integer.parseInt(getGlobalPref("equalizer.number_of_bands", "5")); + final int[] centerFreqs = Constants.getCenterFreqs(mContext, bands); + final int[] bandLevelRange = Constants.getBandLevelRange(mContext); + + float[] centerFreqsKHz = new float[centerFreqs.length]; + for (int i = 0; i < centerFreqs.length; i++) { + centerFreqsKHz[i] = (float) centerFreqs[i] / 1000.0f; + } + + mMinDB = bandLevelRange[0] / 100; + mMaxDB = bandLevelRange[1] / 100; + + mNumBands = centerFreqsKHz.length; + mGlobalLevels = new float[mNumBands]; + for (int i = 0; i < mGlobalLevels.length; i++) { + mGlobalLevels[i] = 0; + } + + mZeroedBandString = EqUtils.getZeroedBandsString(getNumBands()); + + 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; + + // setup equalizer presets + final int numPresets = Integer.parseInt(getGlobalPref("equalizer.number_of_presets", "0")); + + if (numPresets > 0) { + // add library-provided presets + String[] presetNames = getGlobalPref("equalizer.preset_names", "").split("\\|"); + mPredefinedPresets = presetNames.length + 1; // we consider first EQ to be part of predefined + for (int i = 0; i < numPresets; i++) { + mEqPresets.add(new Preset.StaticPreset(presetNames[i], getPersistedPresetLevels(i))); + } + } else { + mPredefinedPresets = 1; // custom is predefined + } + // add custom preset + mEqPresets.add(new Preset.PermCustomPreset(mContext.getString(R.string.user), + getPersistedCustomLevels())); + mEQCustomPresetPosition = mEqPresets.size() - 1; + + // restore custom prefs + mEqPresets.addAll(Constants.getCustomPresets(mContext, mNumBands)); + + // setup default preset for speaker + mCurrentPreset = Integer.parseInt(getPref(Constants.DEVICE_AUDIOFX_EQ_PRESET, "0")); + if (mCurrentPreset > mEqPresets.size() - 1) { + mCurrentPreset = 0; + } + setPreset(mCurrentPreset); + } + + public boolean isUserPreset() { + boolean result = mCurrentPreset >= mPredefinedPresets; + /*if (DEBUG) { + Log.i(TAG, "isUserPreset(), current preset: " + mCurrentPreset); + Log.i(TAG, "----> predefined presets: " + mPredefinedPresets); + Log.d(TAG, "----> RESULT: " + result); + }*/ + return result; + } + + public boolean isCustomPreset() { + return mCurrentPreset == mEQCustomPresetPosition; + } + + public CompoundButton.OnCheckedChangeListener getLockChangeListener() { + if (mLockChangeListener == null) { + mLockChangeListener = new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isUserPreset()) { + ((Preset.CustomPreset) mEqPresets.get(mCurrentPreset)).setLocked(isChecked); + } + } + }; + } + return mLockChangeListener; + } + + public boolean isChangingPresets() { + return mChangingPreset; + } + + public void setChangingPresets(boolean changing) { + if (mChangingPreset != changing) { + mChangingPreset = changing; + if (changing) { + mConfig.getCallbacks().notifyEqControlStateChanged(false, false, false, false); + } else { + updateEqControls(); + } + } + } + + public boolean isAnimatingToCustom() { + return mAnimatingToCustom.get(); + } + + public void setAnimatingToCustom(boolean animating) { + mAnimatingToCustom.set(animating); + if (!animating) { + // finished animation + updateEqControls(); + } + } + + private void savePresetsDelayed() { + mHandler.sendEmptyMessageDelayed(MSG_SAVE_PRESETS, 500); + } + + public int indexOf(Preset p) { + return mEqPresets.indexOf(p); + } + + void onPreDeviceChanged() { + // need to update the current preset based on the device here. + int newPreset = Integer.parseInt(getPref(Constants.DEVICE_AUDIOFX_EQ_PRESET, "0")); + if (newPreset > mEqPresets.size() - 1) { + newPreset = 0; + } + + // this should be ready to go for callbacks to query the new device preset below + mCurrentPreset = newPreset; + + } + + void onPostDeviceChanged() { + setPreset(mCurrentPreset, false); + } + + public Preset getCurrentPreset() { + return mEqPresets.get(mCurrentPreset); + } + + /** + * Copy current config levels from the current preset into custom values since the user has + * initiated some change. Then update the current preset to 'custom'. + */ + public int copyToCustom() { + updateGlobalLevels(mCurrentPreset); + if (DEBUG) { + Log.w(TAG, "using levels from preset: " + mCurrentPreset + ": " + Arrays.toString(mGlobalLevels)); + } + + String levels = EqUtils.floatLevelsToString( + EqUtils.convertDecibelsToMillibels( + mEqPresets.get(mCurrentPreset).getLevels())); + setGlobalPref("custom", levels); + + ((Preset.PermCustomPreset) mEqPresets.get(mEQCustomPresetPosition)).setLevels(mGlobalLevels); + if (DEBUG) + Log.i(TAG, "copyToCustom() wrote current preset levels to index: " + mEQCustomPresetPosition); + setPreset(mEQCustomPresetPosition); + savePresetsDelayed(); + return mEQCustomPresetPosition; + } + + public int addPresetFromCustom() { + updateGlobalLevels(mEQCustomPresetPosition); + if (DEBUG) { + Log.w(TAG, "using levels from preset: " + mCurrentPreset + ": " + Arrays.toString(mGlobalLevels)); + } + + int writtenToIndex = addPreset(mGlobalLevels); + if (DEBUG) + Log.i(TAG, "addPresetFromCustom() wrote current preset levels to index: " + writtenToIndex); + setPreset(writtenToIndex); + savePresetsDelayed(); + return writtenToIndex; + } + + /** + * Loops through all presets. And finds the first preset that can be written to. + * If one is not found, then one is inserted, and that new index is returned. + * @return the index that the levels were copied to + */ + private int addPreset(float[] levels) { + if (UserSession.getInstance() != null) { + UserSession.getInstance().presetCreated(); + } + + final int customPresets = Constants.getCustomPresets(mContext, mNumBands).size(); + // format the name so it's like "Custom <N>", start with "Custom 2" + final String name = String.format(mContext.getString(R.string.user_n), customPresets + 2); + + Preset.CustomPreset customPreset = new Preset.CustomPreset(name, levels, false); + mEqPresets.add(customPreset); + + mConfig.getCallbacks().notifyPresetsChanged(); + + return mEqPresets.size() - 1; + } + + /** + * Set a new level! + * <p/> + * This call will be propogated to all listeners registered with addEqStateChangeCallback(). + * + * @param band the band index the band index which changed + * @param dB the new decibel value + * @param systemChange is this change generated by the system? + */ + public void setLevel(final int band, final float dB, final boolean fromSystem) { + if (DEBUG) Log.i(TAG, "setLevel(" + band + ", " + dB + ", " + fromSystem + ")"); + + mGlobalLevels[band] = dB; + + if (fromSystem && !mConfig.isUserDeviceOverride()) { + // quickly convert decibel to millibel and send away to the service + mHandler.obtainMessage(MSG_SEND_EQ_OVERRIDE, band, (short) (dB * 100)).sendToTarget(); + } + + mConfig.getCallbacks().notifyBandLevelChangeChanged(band, dB, fromSystem); + + if (!fromSystem) { // user is touching + // persist + + final Preset preset = mEqPresets.get(mCurrentPreset); + if (preset instanceof Preset.CustomPreset) { + if (mAnimatingToCustom.get()) { + if (DEBUG) { + Log.d(TAG, "setLevel() not persisting new custom band becuase animating."); + } + } else { + ((Preset.CustomPreset) preset).setLevel(band, dB); + if (preset instanceof Preset.PermCustomPreset) { + // store these as millibels + String levels = EqUtils.floatLevelsToString( + EqUtils.convertDecibelsToMillibels( + preset.getLevels())); + setGlobalPref("custom", levels); + } + } + // needs to be updated immediately here for the service. + final String levels = EqUtils.floatLevelsToString(preset.getLevels()); + setPref(Constants.DEVICE_AUDIOFX_EQ_PRESET_LEVELS, levels); + + mConfig.updateService(AudioFxService.EQ_CHANGED); + } + savePresetsDelayed(); + } + } + + /** + * Set a new preset index. + * <p/> + * This call will be propogated to all listeners registered with addEqStateChangeCallback(). + * + * @param newPresetIndex the new preset index. + */ + public void setPreset(final int newPresetIndex, boolean updateBackend) { + mCurrentPreset = newPresetIndex; + updateEqControls(); // do this before callback is propogated + + mConfig.getCallbacks().notifyPresetChanged(newPresetIndex); + + // persist + setPref(Constants.DEVICE_AUDIOFX_EQ_PRESET, String.valueOf(newPresetIndex)); + + // update mGlobalLevels + float[] newlevels = getPresetLevels(newPresetIndex); + for (int i = 0; i < newlevels.length; i++) { + setLevel(i, newlevels[i], true); + } + + setPref(Constants.DEVICE_AUDIOFX_EQ_PRESET_LEVELS, EqUtils.floatLevelsToString(newlevels)); + + if (updateBackend) { + mConfig.updateService(AudioFxService.EQ_CHANGED); + } + } + + public void setPreset(final int newPresetIndex) { + setPreset(newPresetIndex, true); + } + + private void updateEqControls() { + final boolean userPreset = isUserPreset(); + mConfig.getCallbacks().notifyEqControlStateChanged(mEQCustomPresetPosition == mCurrentPreset, + userPreset, userPreset, userPreset); + } + + /** + * @return Get the current preset index + */ + public int getCurrentPresetIndex() { + return mCurrentPreset; + } + + /*=============== + * eq methods + *===============*/ + + public 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)); + } + + public double reverseProjectX(float pos) { + double minPos = Math.log(mMinFreq); + double maxPos = Math.log(mMaxFreq); + return Math.exp(pos * (maxPos - minPos) + minPos); + } + + public float projectY(double dB) { + double pos = (dB - mMinDB) / (mMaxDB - mMinDB); + return (float) (1 - pos); + } + + public static double lin2dB(double rho) { + return rho != 0 ? Math.log(rho) / Math.log(10) * 20 : -99.9; + } + + public float getMinFreq() { + return mMinFreq; + } + + public float getMaxFreq() { + return mMaxFreq; + } + + public float getMinDB() { + return mMinDB; + } + + public float getMaxDB() { + return mMaxDB; + } + + public int getNumBands() { + return mNumBands; + } + + public float getCenterFreq(int band) { + return mCenterFreqs[band]; + } + + public float[] getCenterFreqs() { + return mCenterFreqs; + } + + public float[] getLevels() { + return mGlobalLevels; + } + + public float getLevel(int band) { + return mGlobalLevels[band]; + } + + /*=============== + * preset methods + *===============*/ + + public float[] getPersistedPresetLevels(int presetIndex) { + String newLevels = null; + + if (mEqPresets.size() > presetIndex + && mEqPresets.get(presetIndex) instanceof Preset.PermCustomPreset) { + return getPersistedCustomLevels(); + } else { + newLevels = getGlobalPref("equalizer.preset." + presetIndex, mZeroedBandString); + } + + // stored as millibels, convert to decibels + float[] levels = EqUtils.stringBandsToFloats(newLevels); + return EqUtils.convertMillibelsToDecibels(levels); + } + + private float[] getPersistedCustomLevels() { + String newLevels = getGlobalPref("custom", mZeroedBandString); + // stored as millibels, convert to decibels + float[] levels = EqUtils.stringBandsToFloats(newLevels); + return EqUtils.convertMillibelsToDecibels(levels); + } + + /** + * Get preset levels in decibels for a given index + * + * @param presetIndex index which to fetch preset levels for + * @return an array of floats[] with the given index's preset levels + */ + public float[] getPresetLevels(int presetIndex) { + return mEqPresets.get(presetIndex).getLevels(); + } + + /** + * Helper method which maps a preset index to a color value. + * + * @param index the preset index which to fetch a color for + * @return a color which is associated with this preset. + */ + public int getAssociatedPresetColorHex(int index) { + int r = -1; + index = index % mEqPresets.size(); + if (mEqPresets.get(index) instanceof Preset.CustomPreset) { + r = R.color.preset_custom; + } else { + switch (index) { + case 0: + r = R.color.preset_normal; + break; + case 1: + r = R.color.preset_classical; + break; + case 2: + r = R.color.preset_dance; + break; + case 3: + r = R.color.preset_flat; + break; + case 4: + r = R.color.preset_folk; + break; + case 5: + r = R.color.preset_metal; + break; + case 6: + r = R.color.preset_hiphop; + break; + case 7: + r = R.color.preset_jazz; + break; + case 8: + r = R.color.preset_pop; + break; + case 9: + r = R.color.preset_rock; + break; + case 10: + r = R.color.preset_electronic; + break; + case 11: + r = R.color.preset_small_speakers; + break; + default: + return r; + } + } + return mContext.getResources().getColor(r); + } + + /** + * Get total number of presets + * + * @return int value with total number of presets + */ + public int getPresetCount() { + return mEqPresets.size(); + } + + public Preset getPreset(int index) { + return mEqPresets.get(index); + } + + public String getLocalizedPresetName(int index) { + // already localized + return localizePresetName(mEqPresets.get(index).getName()); + } + + private final String localizePresetName(final String name) { + // missing electronic, multimedia, small speakers, custom + final String[] names = { + "Normal", "Classical", "Dance", "Flat", "Folk", + "Heavy Metal", "Hip Hop", "Jazz", "Pop", "Rock", + "Electronic", "Small speakers", "Multimedia", + "Custom" + }; + 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, + R.string.ci_extreme, R.string.small_speakers, R.string.multimedia, + R.string.user + }; + + for (int i = names.length - 1; i >= 0; --i) { + if (names[i].equalsIgnoreCase(name)) { + return mContext.getString(ids[i]); + } + } + return name; + } + + public boolean isEqualizerLocked() { + return getCurrentPreset() instanceof Preset.CustomPreset + && !(getCurrentPreset() instanceof Preset.PermCustomPreset) + && ((Preset.CustomPreset) getCurrentPreset()).isLocked(); + } + + public void renameCurrentPreset(String s) { + if (UserSession.getInstance() != null) { + UserSession.getInstance().presetRenamed(); + } + + if (isUserPreset()) { + ((Preset.CustomPreset) getCurrentPreset()).setName(s); + } + + mConfig.getCallbacks().notifyPresetsChanged(); + + savePresetsDelayed(); + } + + public boolean removePreset(int index) { + if (UserSession.getInstance() != null) { + UserSession.getInstance().presetRemoved(); + } + + if (index > mEQCustomPresetPosition) { + mEqPresets.remove(index); + mConfig.getCallbacks().notifyPresetsChanged(); + + if (mCurrentPreset == index) { + if (DEBUG) { + Log.w(TAG, "removePreset() called on current preset, changing preset"); + } + updateGlobalLevels(mCurrentPreset - 1); + setPreset(mCurrentPreset - 1); + } + savePresetsDelayed(); + return true; + } + return false; + } + + private void updateGlobalLevels(int presetIndexToCopy) { + final float[] presetLevels = getPresetLevels(presetIndexToCopy); + for (int i = 0; i < mGlobalLevels.length; i++) { + mGlobalLevels[i] = presetLevels[i]; + } + } + + // I AM SO LAZY! + private String getGlobalPref(String key, String defValue) { + return mConfig.getGlobalPrefs().getString(key, defValue); + } + + private void setGlobalPref(String key, String value) { + mConfig.getGlobalPrefs().edit().putString(key, value).apply(); + } + + private String getPref(String key, String defValue) { + return mConfig.getPrefs().getString(key, defValue); + } + + private void setPref(String key, String value) { + mConfig.getPrefs().edit().putString(key, value).apply(); + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/activity/MasterConfigControl.java b/src/org/cyanogenmod/audiofx/audiofx/activity/MasterConfigControl.java new file mode 100644 index 0000000..d83d0de --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/activity/MasterConfigControl.java @@ -0,0 +1,367 @@ +package com.cyngn.audiofx.activity; + +import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; +import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_SCO; +import static android.media.AudioDeviceInfo.TYPE_DOCK; +import static android.media.AudioDeviceInfo.TYPE_IP; +import static android.media.AudioDeviceInfo.TYPE_LINE_ANALOG; +import static android.media.AudioDeviceInfo.TYPE_LINE_DIGITAL; +import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY; +import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE; +import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES; +import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET; +import static android.media.AudioDeviceInfo.convertDeviceTypeToInternalDevice; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.IBinder; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import com.cyngn.audiofx.Constants; +import com.cyngn.audiofx.service.AudioFxService; + +import java.util.ArrayList; +import java.util.List; + +/** + * Master configuration class for AudioFX. + * + * Contains the main hub where data is stored for the current eq graph (which there should be + * one of, thus only once instance of this class exists). + * + * Anyone can obtain an instance of this class. If one does not exist, a new one is created. + * Immediately before the new instance creation happens, some defaults are pre-populated + * with MasterConfigControl.saveDefaults(). That method doesn't ever have to be directly called. + */ +public class MasterConfigControl { + + private static final String TAG = MasterConfigControl.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final boolean SERVICE_DEBUG = false; + + private final Context mContext; + + private AudioFxService.LocalBinder mService; + private ServiceConnection mServiceConnection; + private int mServiceRefCount = 0; + + private AudioDeviceInfo mCurrentDevice; + private AudioDeviceInfo mUserDeviceOverride; + + private final StateCallbacks mCallbacks; + private final EqualizerManager mEqManager; + private final AudioManager mAudioManager; + + private static MasterConfigControl sInstance; + private boolean mShouldBindToService = false; + + public static MasterConfigControl getInstance(Context context) { + if (sInstance == null) { + sInstance = new MasterConfigControl(context); + } + return sInstance; + } + + private MasterConfigControl(Context context) { + mContext = context.getApplicationContext(); + + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + mCallbacks = new StateCallbacks(this); + mEqManager = new EqualizerManager(context, this); + } + + public void onResetDefaults() { + mEqManager.applyDefaults(); + } + + public synchronized boolean bindService() { + boolean conn = true; + if (SERVICE_DEBUG) Log.i(TAG, "bindService() refCount=" + mServiceRefCount); + if (mServiceConnection == null && mServiceRefCount == 0) { + mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + if (SERVICE_DEBUG) Log.i(TAG, "onServiceConnected refCount=" + mServiceRefCount); + mService = ((AudioFxService.LocalBinder) binder); + LocalBroadcastManager.getInstance(mContext).registerReceiver( + mDeviceChangeReceiver, + new IntentFilter(AudioFxService.ACTION_DEVICE_OUTPUT_CHANGED)); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + if (SERVICE_DEBUG) Log.w(TAG, "onServiceDisconnected refCount =" + mServiceRefCount); + LocalBroadcastManager.getInstance(mContext).unregisterReceiver( + mDeviceChangeReceiver); + mService = null; + } + }; + + Intent serviceIntent = new Intent(mContext, AudioFxService.class); + conn = mContext.bindService(serviceIntent, mServiceConnection, + Context.BIND_AUTO_CREATE); + } + if (conn) { + mServiceRefCount++; + } + return mServiceRefCount > 0; + } + + public synchronized void unbindService() { + if (SERVICE_DEBUG) Log.i(TAG, "unbindService() called refCount=" + mServiceRefCount); + if (mServiceRefCount > 0) { + mServiceRefCount--; + if (mServiceRefCount == 0) { + mContext.unbindService(mServiceConnection); + mService = null; + mServiceConnection = null; + } + } + } + + public boolean checkService() { + if (mService == null && mServiceRefCount == 0 && mShouldBindToService) { + Log.e(TAG, "Service went away, rebinding"); + bindService(); + } + return mService != null; + } + + private final BroadcastReceiver mDeviceChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int device = intent.getIntExtra("device", -1); + Log.d(TAG, "deviceChanged: " + device); + if (device > -1) { + AudioDeviceInfo info = getDeviceById(device); + if (info != null) { + setCurrentDevice(info, false); + } + } + } + }; + + public void updateService(int flags) { + if (checkService()) { + mService.update(flags); + } + } + + public StateCallbacks getCallbacks() { + return mCallbacks; + } + + public EqualizerManager getEqualizerManager() { + return mEqManager; + } + + public synchronized void setCurrentDeviceEnabled(boolean isChecked) { + getPrefs().edit().putBoolean(Constants.DEVICE_AUDIOFX_GLOBAL_ENABLE, isChecked).apply(); + getCallbacks().notifyGlobalToggle(isChecked); + updateService(AudioFxService.ALL_CHANGED); + } + + public synchronized boolean isCurrentDeviceEnabled() { + return getPrefs().getBoolean(Constants.DEVICE_AUDIOFX_GLOBAL_ENABLE, false); + } + + public synchronized SharedPreferences getGlobalPrefs() { + return mContext.getSharedPreferences(Constants.AUDIOFX_GLOBAL_FILE, 0); + } + + /** + * Update the current device used when querying any device-specific values such as the current + * preset, or the user's custom eq preset settings. + * + * @param audioOutputRouting the new device key + */ + public synchronized void setCurrentDevice(AudioDeviceInfo device, final boolean userSwitch) { + + final AudioDeviceInfo current = getCurrentDevice(); + + Log.d(TAG, "setCurrentDevice name=" + (current == null ? null : current.getProductName()) + + " fromUser=" + userSwitch + + " cur=" + (current == null ? null : current.getType()) + + " new=" + (device == null ? null : device.getType())); + + if (userSwitch) { + mUserDeviceOverride = device; + } else { + if (device != null) { + mCurrentDevice = device; + } + mUserDeviceOverride = null; + } + + mEqManager.onPreDeviceChanged(); + + mCallbacks.notifyDeviceChanged(device, userSwitch); + + mEqManager.onPostDeviceChanged(); + } + + public AudioDeviceInfo getSystemDevice() { + if (mCurrentDevice == null) { + final int forMusic = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC); + for (AudioDeviceInfo ai : getConnectedDevices()) { + if ((convertDeviceTypeToInternalDevice(ai.getType()) & forMusic) > 0) { + return ai; + } + } + } + return mCurrentDevice; + } + + public boolean isUserDeviceOverride() { + return mUserDeviceOverride != null; + } + + public AudioDeviceInfo getCurrentDevice() { + if (isUserDeviceOverride()) { + return mUserDeviceOverride; + } + return getSystemDevice(); + } + + public AudioDeviceInfo getDeviceById(int id) { + for (AudioDeviceInfo ai : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { + if (ai.getId() == id) { + return ai; + } + } + return null; + } + + public List<AudioDeviceInfo> getConnectedDevices(int... filter) { + final List<AudioDeviceInfo> devices = new ArrayList<AudioDeviceInfo>(); + for (AudioDeviceInfo ai : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { + if (filter.length == 0) { + devices.add(ai); + } else { + for (int i = 0; i < filter.length; i++) { + if (ai.getType() == filter[i]) { + devices.add(ai); + continue; + } + } + } + } + return devices; + } + + public String getCurrentDeviceIdentifier() { + return getDeviceIdentifierString(getCurrentDevice()); + } + + public SharedPreferences getPrefs() { + return mContext.getSharedPreferences(getCurrentDeviceIdentifier(), 0); + } + + public boolean hasDts() { + return getGlobalPrefs().getBoolean(Constants.AUDIOFX_GLOBAL_HAS_DTS, false); + } + + public boolean hasMaxxAudio() { + return getGlobalPrefs().getBoolean(Constants.AUDIOFX_GLOBAL_HAS_MAXXAUDIO, false); + } + + public boolean getMaxxVolumeEnabled() { + return getPrefs().getBoolean(Constants.DEVICE_AUDIOFX_MAXXVOLUME_ENABLE, false); + } + + public boolean hasBassBoost() { + return getGlobalPrefs().getBoolean(Constants.AUDIOFX_GLOBAL_HAS_BASSBOOST, false); + } + + public boolean hasVirtualizer() { + return getGlobalPrefs().getBoolean(Constants.AUDIOFX_GLOBAL_HAS_VIRTUALIZER, false); + } + + public void setMaxxVolumeEnabled(boolean enable) { + getPrefs().edit().putBoolean(Constants.DEVICE_AUDIOFX_MAXXVOLUME_ENABLE, enable).apply(); + updateService(AudioFxService.VOLUME_BOOST_CHANGED); + } + + void overrideEqLevels(short band, short level) { + if (checkService()) { + mService.setOverrideLevels(band, level); + } + } + + public static String getDeviceDisplayString(Context context, AudioDeviceInfo info) { + int type = info == null ? -1 : info.getType(); + switch (type) { + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + return context.getString(com.cyngn.audiofx.R.string.device_headset); + case TYPE_LINE_ANALOG: + case TYPE_LINE_DIGITAL: + return context.getString(com.cyngn.audiofx.R.string.device_line_out); + case TYPE_BLUETOOTH_SCO: + case TYPE_BLUETOOTH_A2DP: + case TYPE_USB_DEVICE: + case TYPE_USB_ACCESSORY: + case TYPE_DOCK: + case TYPE_IP: + return info.getProductName().toString(); + default: + return context.getString(com.cyngn.audiofx.R.string.device_speaker); + } + } + + private static String appendProductName(AudioDeviceInfo info, String prefix) { + StringBuilder nm = new StringBuilder(prefix); + if (info != null && info.getProductName() != null) { + nm.append("-").append(info.getProductName().toString().replaceAll("\\W+", "")); + } + return nm.toString(); + } + + private static String appendDeviceAddress(AudioDeviceInfo info, String prefix) { + StringBuilder nm = new StringBuilder(prefix); + if (info != null && info.getAddress() != null) { + nm.append("-").append(info.getAddress().replace(":", "")); + } + return nm.toString(); + } + + public static String getDeviceIdentifierString(AudioDeviceInfo info) { + int type = info == null ? -1 : info.getType(); + switch (type) { + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + return Constants.DEVICE_HEADSET; + case TYPE_LINE_ANALOG: + case TYPE_LINE_DIGITAL: + return Constants.DEVICE_LINE_OUT; + case TYPE_BLUETOOTH_SCO: + case TYPE_BLUETOOTH_A2DP: + return appendDeviceAddress(info, Constants.DEVICE_PREFIX_BLUETOOTH); + case TYPE_USB_DEVICE: + case TYPE_USB_ACCESSORY: + case TYPE_DOCK: + return appendProductName(info, Constants.DEVICE_PREFIX_USB); + case TYPE_IP: + return appendProductName(info, Constants.DEVICE_PREFIX_CAST); + default: + return Constants.DEVICE_SPEAKER; + } + } + + /** + * Set whether to automatically attempt to bind to the service. + * @param bindToService + */ + public void setAutoBindToService(boolean bindToService) { + mShouldBindToService = bindToService; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/activity/StateCallbacks.java b/src/org/cyanogenmod/audiofx/audiofx/activity/StateCallbacks.java new file mode 100644 index 0000000..0aa7379 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/activity/StateCallbacks.java @@ -0,0 +1,154 @@ + +package com.cyngn.audiofx.activity; + +import android.media.AudioDeviceInfo; + +import java.util.ArrayList; +import java.util.List; + +public class StateCallbacks { + + private static final String TAG = "StateCallbacks"; + + private final MasterConfigControl mConfig; + + private final List<EqUpdatedCallback> mEqUpdateCallbacks = new ArrayList<EqUpdatedCallback>(); + + private final List<DeviceChangedCallback> mDeviceChangedCallbacks = new ArrayList<DeviceChangedCallback>(); + + private final List<EqControlStateCallback> mEqControlStateCallbacks = new ArrayList<EqControlStateCallback>(); + + StateCallbacks(MasterConfigControl config) { + mConfig = config; + } + + /** + * Implement this callback to receive any changes called to the + * MasterConfigControl instance + */ + public interface EqUpdatedCallback { + /** + * A band level has been changed + * + * @param band the band index which changed + * @param dB the new decibel value + * @param fromSystem whether the event was from the system or from the + * user + */ + public void onBandLevelChange(int band, float dB, boolean fromSystem); + + /** + * The preset has been set + * + * @param newPresetIndex the new preset index. + */ + public void onPresetChanged(int newPresetIndex); + + public void onPresetsChanged(); + } + + public void addEqUpdatedCallback(EqUpdatedCallback callback) { + synchronized (mEqUpdateCallbacks) { + mEqUpdateCallbacks.add(callback); + } + } + + public void removeEqUpdatedCallback(EqUpdatedCallback callback) { + synchronized (mEqUpdateCallbacks) { + mEqUpdateCallbacks.remove(callback); + } + } + + void notifyPresetsChanged() { + synchronized (mEqUpdateCallbacks) { + for (final EqUpdatedCallback callback : mEqUpdateCallbacks) { + callback.onPresetsChanged(); + } + } + } + + void notifyPresetChanged(final int index) { + synchronized (mEqUpdateCallbacks) { + for (final EqUpdatedCallback callback : mEqUpdateCallbacks) { + callback.onPresetChanged(index); + } + } + } + + void notifyBandLevelChangeChanged(final int band, final float dB, final boolean fromSystem) { + synchronized (mEqUpdateCallbacks) { + for (final EqUpdatedCallback callback : mEqUpdateCallbacks) { + callback.onBandLevelChange(band, dB, fromSystem); + } + } + } + + /** + * Callback for changes to visibility and state of the EQ + */ + public interface EqControlStateCallback { + public void updateEqState(boolean saveVisible, boolean removeVisible, + boolean renameVisible, boolean unlockVisible); + } + + public void addEqControlStateCallback(EqControlStateCallback callback) { + synchronized (mEqControlStateCallbacks) { + mEqControlStateCallbacks.add(callback); + } + } + + public synchronized void removeEqControlStateCallback(EqControlStateCallback callback) { + synchronized (mEqControlStateCallbacks) { + mEqControlStateCallbacks.remove(callback); + } + } + + void notifyEqControlStateChanged(boolean saveVisible, boolean removeVisible, + boolean renameVisible, boolean unlockVisible) { + synchronized (mEqControlStateCallbacks) { + for (final EqControlStateCallback callback : mEqControlStateCallbacks) { + callback.updateEqState(saveVisible, removeVisible, renameVisible, unlockVisible); + } + } + } + + /** + * Register this callback to receive notification when the output device + * changes. + */ + public interface DeviceChangedCallback { + public void onDeviceChanged(AudioDeviceInfo device, boolean userChange); + public void onGlobalDeviceToggle(boolean on); + + } + + public void addDeviceChangedCallback(DeviceChangedCallback callback) { + synchronized (mDeviceChangedCallbacks) { + mDeviceChangedCallbacks.add(callback); + callback.onDeviceChanged(mConfig.getCurrentDevice(), false); + } + } + + public synchronized void removeDeviceChangedCallback(DeviceChangedCallback callback) { + synchronized (mDeviceChangedCallbacks) { + mDeviceChangedCallbacks.remove(callback); + } + } + + void notifyGlobalToggle(boolean on) { + synchronized (mDeviceChangedCallbacks) { + for (DeviceChangedCallback callback : mDeviceChangedCallbacks) { + callback.onGlobalDeviceToggle(on); + } + + } + } + + void notifyDeviceChanged(final AudioDeviceInfo newDevice, final boolean fromUser) { + synchronized (mDeviceChangedCallbacks) { + for (final DeviceChangedCallback callback : mDeviceChangedCallbacks) { + callback.onDeviceChanged(newDevice, fromUser); + } + } + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/backends/AndroidEffects.java b/src/org/cyanogenmod/audiofx/audiofx/backends/AndroidEffects.java new file mode 100644 index 0000000..090eb73 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/backends/AndroidEffects.java @@ -0,0 +1,185 @@ +package com.cyngn.audiofx.backends; + +import android.media.AudioDeviceInfo; +import android.media.audiofx.AudioEffect; +import android.media.audiofx.BassBoost; +import android.media.audiofx.PresetReverb; +import android.media.audiofx.Virtualizer; +import android.util.Log; + +import com.cyngn.audiofx.Constants; + +/** + * EffectSet which comprises standard Android effects + */ +class AndroidEffects extends EffectSetWithAndroidEq { + + /** + * Session-specific bassboost + */ + private BassBoost mBassBoost; + + /** + * Session-specific virtualizer + */ + private Virtualizer mVirtualizer; + + /** + * Session-specific reverb + */ + private PresetReverb mPresetReverb; + + public AndroidEffects(int sessionId, AudioDeviceInfo deviceInfo) { + super(sessionId, deviceInfo); + } + + @Override + protected void onCreate() { + super.onCreate(); + + mBassBoost = new BassBoost(100, mSessionId); + mVirtualizer = new Virtualizer(100, mSessionId); + mPresetReverb = new PresetReverb(100, mSessionId); + } + + @Override + public void release() { + super.release(); + + try { + if (mBassBoost != null) { + mBassBoost.release(); + } + } catch (Exception e) { + // ignored; + } + try { + if (mVirtualizer != null) { + mVirtualizer.release(); + } + } catch (Exception e) { + // ignored + } + try { + if (mPresetReverb != null) { + mPresetReverb.release(); + } + } catch (Exception e) { + // ignored + } + mBassBoost = null; + mVirtualizer = null; + mPresetReverb = null; + } + + @Override + public synchronized void setDevice(AudioDeviceInfo deviceInfo) { + super.setDevice(deviceInfo); + } + + @Override + public void setGlobalEnabled(boolean globalEnabled) { + super.setGlobalEnabled(globalEnabled); + + if (!globalEnabled) { + // disable everything. it will get explictly enabled + // individually when necessary. + try { + if (mVirtualizer != null) { + mVirtualizer.setEnabled(false); + } + } catch (Exception e) { + Log.e(TAG, "Unable to disable virtualizer!", e); + } + try { + if (mBassBoost != null) { + mBassBoost.setEnabled(false); + } + } catch (Exception e) { + Log.e(TAG, "Unable to disable bass boost!", e); + } + try { + if (mPresetReverb != null) { + mPresetReverb.setEnabled(false); + } + } catch (Exception e) { + Log.e(TAG, "Unable to disable reverb!", e); + } + } + } + + @Override + public boolean hasVirtualizer() { + return mVirtualizer != null && mVirtualizer.getStrengthSupported(); + } + + @Override + public boolean hasBassBoost() { + return mBassBoost != null && mBassBoost.getStrengthSupported(); + } + + @Override + public void enableBassBoost(boolean enable) { + try { + if (mBassBoost != null) { + mBassBoost.setEnabled(enable); + } + } catch (Exception e) { + Log.e(TAG, "Unable to " + (enable ? "enable" : "disable") + " bass boost!", e); + } + } + + @Override + public void setBassBoostStrength(short strength) { + setParameterSafe(mBassBoost, BassBoost.PARAM_STRENGTH, strength); + } + + @Override + public void enableVirtualizer(boolean enable) { + try { + if (mVirtualizer != null) { + mVirtualizer.setEnabled(enable); + } + } catch (Exception e) { + Log.e(TAG, "Unable to " + (enable ? "enable" : "disable") + " virtualizer!", e); + } + } + + @Override + public void setVirtualizerStrength(short strength) { + setParameterSafe(mVirtualizer, Virtualizer.PARAM_STRENGTH, strength); + } + + @Override + public void enableReverb(boolean enable) { + try { + if (mPresetReverb != null) { + mPresetReverb.setEnabled(enable); + } + } catch (Exception e) { + Log.e(TAG, "Unable to " + (enable ? "enable" : "disable") + " preset reverb!", e); + } + + } + + @Override + public void setReverbPreset(short preset) { + setParameterSafe(mPresetReverb, PresetReverb.PARAM_PRESET, preset); + } + + @Override + public int getBrand() { + return Constants.EFFECT_TYPE_ANDROID; + } + + private void setParameterSafe(AudioEffect e, int p, short v) { + if (e == null) { + return; + } + try { + e.setParameter(p, v); + } catch (Exception ex) { + Log.e(TAG, "Failed to set param " + p + " for effect " + e.getDescriptor().name, ex); + } + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/backends/EffectSet.java b/src/org/cyanogenmod/audiofx/audiofx/backends/EffectSet.java new file mode 100644 index 0000000..47ae894 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/backends/EffectSet.java @@ -0,0 +1,235 @@ +package com.cyngn.audiofx.backends; + +import android.media.AudioDeviceInfo; +import android.util.Log; + +/** + * Helper class representing the full complement of effects attached to one + * audio session. + */ +public abstract class EffectSet { + + protected static final String TAG = "AudioFx-EffectSet"; + + protected final int mSessionId; + + protected boolean mGlobalEnabled; + + private AudioDeviceInfo mDeviceInfo; + + private boolean mMarkedForDeath = false; + + public EffectSet(int sessionId, AudioDeviceInfo deviceInfo) { + mSessionId = sessionId; + mDeviceInfo = deviceInfo; + try { + onCreate(); + } catch (Exception e) { + Log.e(TAG, "error creating" + this + ", releasing and throwing!"); + release(); + throw e; + } + } + + /** + * Called to do subclass-first initialization in case + * an implementation has ordering restrictions. + * + * This call is wrapped in a try/catch - if an exception is thrown here, + * a release will immediately be called. + */ + protected void onCreate() { } + + /** + * Destroy all effects in this set. + * + * Attempting to use this object after calling release is + * undefined behavior. + */ + public void release() { } + + /** + * Returns the enumerated brand of this implementation + * @return brandId + */ + public abstract int getBrand(); + + /** + * Called when the user toggles the engine on or off. If the + * implementation has a built-in bypass mode, this is where + * to use it. + * + * @param globalEnabled + */ + public void setGlobalEnabled(boolean globalEnabled) { + mGlobalEnabled = globalEnabled; + } + + public boolean isGlobalEnabled() { + return mGlobalEnabled; + } + + /** + * Called when the output device has changed. All cached + * data should be cleared at this point. + * + * @param deviceInfo + */ + public void setDevice(AudioDeviceInfo deviceInfo) { + mDeviceInfo = deviceInfo; + } + + /** + * Return the current active output device + * @return deviceInfo + */ + public AudioDeviceInfo getDevice() { + return mDeviceInfo; + } + + /** + * Begin bulk-update of parameters. This can be used if the + * implementation supports operation in a transactional/atomic + * manner. Parameter changes will immediately follow this call + * and should be committed to the backend when the subsequent + * commitUpdate() is called. + * + * Optional. + * + * @return status - false on failure + */ + public boolean beginUpdate() { return true; } + + /** + * Commit accumulated updates to the backend. See above. + * + * begin/commit are used when a large number of parameters need + * to be sent to the backend, such as in the case of a device + * switch or preset change. This can increase performance and + * reduce click/pop issues. + * + * Optional. + * + * @return status - false on failure + */ + public boolean commitUpdate() { return true; } + + /* ---- Top level effects begin here ---- */ + + // required effects + public abstract boolean hasVirtualizer(); + + public abstract boolean hasBassBoost(); + + // optional effects + public boolean hasTrebleBoost() { + return false; + } + + public boolean hasVolumeBoost() { + return false; + } + + public boolean hasReverb() { + return false; + } + + public abstract void enableEqualizer(boolean enable); + + /** + * @param levels in decibels + */ + public abstract void setEqualizerLevelsDecibels(float[] levels); + + public abstract short getNumEqualizerBands(); + + /** + * @param band + * @param level in millibels + */ + public abstract void setEqualizerBandLevel(short band, float level); + + /** + * @return level in millibels + */ + public abstract int getEqualizerBandLevel(short band); + + public abstract String getEqualizerPresetName(short preset); + + public abstract void useEqualizerPreset(short preset); + + public abstract short getNumEqualizerPresets(); + + public abstract short[] getEqualizerBandLevelRange(); + + /** + * @param band + * @return center frequency of the band in millihertz + */ + public abstract int getCenterFrequency(short band); + + public abstract void enableBassBoost(boolean enable); + + /** + * @param strength with range [0-1000] + */ + public abstract void setBassBoostStrength(short strength); + + public abstract void enableVirtualizer(boolean enable); + + /** + * @param strength with range [0-1000] + */ + public abstract void setVirtualizerStrength(short strength); + + public void enableReverb(boolean enable) { + return; + } + + public void setReverbPreset(short preset) { + return; + } + + public void enableTrebleBoost(boolean enable) { + return; + } + + /** + * @param strength with range [0-100] + */ + public void setTrebleBoostStrength(short strength) { + return; + } + + public void enableVolumeBoost(boolean enable) { + return; + } + + /** + * How long should we delay for when releasing the effects? + * This helps certain effect implementations when the + * app is reusing a session ID. By default this + * behavior is disabled. + */ + public int getReleaseDelay() { + return 0; + } + + public boolean isMarkedForDeath() { + return mMarkedForDeath; + } + + public void setMarkedForDeath(boolean die) { + mMarkedForDeath = die; + } + + @Override + public String toString() { + return "EffectSet (" + this.getClass().getSimpleName() + ")" + + " [ " + + " mSessionId: " + mSessionId + + " mDeviceInfo: " + mDeviceInfo + + " mGlobalEnabled: " + mGlobalEnabled + + " ]"; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/backends/EffectSetWithAndroidEq.java b/src/org/cyanogenmod/audiofx/audiofx/backends/EffectSetWithAndroidEq.java new file mode 100644 index 0000000..81cd53c --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/backends/EffectSetWithAndroidEq.java @@ -0,0 +1,123 @@ +package com.cyngn.audiofx.backends; + +import android.media.AudioDeviceInfo; +import android.media.audiofx.Equalizer; +import android.util.Log; + +import com.cyngn.audiofx.eq.EqUtils; + +/** + * Created by roman on 3/1/16. + */ +public abstract class EffectSetWithAndroidEq extends EffectSet { + /** + * Session-specific equalizer + */ + private Equalizer mEqualizer; + + private short mEqNumPresets = -1; + private short mEqNumBands = -1; + + public EffectSetWithAndroidEq(int sessionId, AudioDeviceInfo deviceInfo) { + super(sessionId, deviceInfo); + } + + @Override + protected void onCreate() { + mEqualizer = new Equalizer(100, mSessionId); + super.onCreate(); + + } + + @Override + public synchronized void release() { + if (mEqualizer != null) { + mEqualizer.release(); + mEqualizer = null; + } + super.release(); + } + + @Override + public void setGlobalEnabled(boolean globalEnabled) { + super.setGlobalEnabled(globalEnabled); + + enableEqualizer(globalEnabled); + } + + @Override + public void enableEqualizer(boolean enable) { + try { + mEqualizer.setEnabled(enable); + } catch (Exception e) { + Log.e(TAG, "enableEqualizer failed! enable=" + enable + " sessionId=" + mSessionId, e); + } + } + + @Override + public void setEqualizerLevelsDecibels(float[] levels) { + final short[] equalizerLevels = EqUtils.convertDecibelsToMillibelsInShorts(levels); + for (short i = 0; i < equalizerLevels.length; i++) { + setBandLevelSafe(i, equalizerLevels[i]); + } + } + + @Override + public short getNumEqualizerBands() { + if (mEqNumBands < 0) { + mEqNumBands = mEqualizer.getNumberOfBands(); + } + return mEqNumBands; + } + + @Override + public void setEqualizerBandLevel(short band, float level) { + setBandLevelSafe(band, (short)level); + } + + @Override + public int getEqualizerBandLevel(short band) { + return mEqualizer.getBandLevel(band); + } + + @Override + public String getEqualizerPresetName(short preset) { + return mEqualizer.getPresetName(preset); + } + + @Override + public void useEqualizerPreset(short preset) { + mEqualizer.usePreset(preset); + } + + @Override + public short getNumEqualizerPresets() { + if (mEqNumPresets < 0) { + mEqNumPresets = mEqualizer.getNumberOfPresets(); + } + return mEqNumPresets; + } + + @Override + public short[] getEqualizerBandLevelRange() { + return mEqualizer.getBandLevelRange(); + } + + @Override + public int getCenterFrequency(short band) { + return mEqualizer.getCenterFreq(band); + } + + @Override + public synchronized void setDevice(AudioDeviceInfo deviceInfo) { + super.setDevice(deviceInfo); + } + + private synchronized void setBandLevelSafe(short band, short level) { + try { + mEqualizer.setBandLevel(band, level); + } catch (Exception e) { + Log.e(TAG, "Unable to set eq band=" + band + " level=" + level, e); + } + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/backends/IEffectFactory.java b/src/org/cyanogenmod/audiofx/audiofx/backends/IEffectFactory.java new file mode 100644 index 0000000..8e629b8 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/backends/IEffectFactory.java @@ -0,0 +1,16 @@ +package com.cyngn.audiofx.backends; + +import android.content.Context; +import android.media.AudioDeviceInfo; + +interface IEffectFactory { + + /** + * Create a new EffectSet based on current stream parameters. + * @param context context to create the effect with + * @param sessionId session id to attach the effect to + * @param currentDevice current device that the effect should initially setup for + * @return an {@link EffectSet} + */ + EffectSet createEffectSet(Context context, int sessionId, AudioDeviceInfo currentDevice); +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/eq/EqBarView.java b/src/org/cyanogenmod/audiofx/audiofx/eq/EqBarView.java new file mode 100644 index 0000000..df697dd --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/eq/EqBarView.java @@ -0,0 +1,208 @@ +package com.cyngn.audiofx.eq; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.EqualizerManager; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.activity.StateCallbacks; + +public class EqBarView extends FrameLayout implements StateCallbacks.EqUpdatedCallback { + + private static final String TAG = EqBarView.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private EqualizerManager mEqManager; + + private float mNormalWidth; + private float mParentHeight = -1; + private float mLastTouchX; + private float mLastTouchY; + private float mPosX; + private float mPosY = -1; + private boolean mUserInteracting; + private int mParentTop; + private Integer mIndex; + private float mInitialLevel; + + public EqBarView(Context context) { + super(context); + init(); + } + + public EqBarView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public EqBarView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mEqManager = MasterConfigControl.getInstance(mContext).getEqualizerManager(); + mNormalWidth = getResources().getDimension(R.dimen.eq_bar_width); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private EqContainerView.EqBandInfo getInfo() { + return (EqContainerView.EqBandInfo) getTag(); + } + + public void setParentHeight(float h, int top) { + mParentHeight = h; + mParentTop = top; + updateHeight(); + } + + void updateHeight() { + if (DEBUG) Log.d(TAG, "updateHeight()"); + + if (getInfo() != null) { + float level = mEqManager.getLevel(getIndex()); + float yProjection = 1 - mEqManager.projectY(level); + float height = (yProjection * (mParentHeight)); + mPosY = height; + + if (DEBUG) { + Log.d(TAG, getIndex() + "level: " + level + ", yProjection: " + + yProjection + ", mPosY: " + mPosY); + } + updateHeight((int) mPosY); + } else { + if (DEBUG) Log.d(TAG, "could not updateHeight()"); + } + } + + public int getIndex() { + if (mIndex == null) { + mIndex = (getInfo()).mIndex; + } + return mIndex; + } + + public boolean isUserInteracting() { + return mUserInteracting; + } + + /* package */ void startInteraction(float x, float y) { + + mLastTouchX = x; + mLastTouchY = y; + mUserInteracting = true; + + if (DEBUG) Log.d(TAG, "initial level: " + mInitialLevel); + mInitialLevel = (1 - (mPosY / mParentHeight)) * (mEqManager.getMinDB() - mEqManager.getMaxDB()) + - mEqManager.getMinDB(); + + updateWidth((int) (mNormalWidth * 2)); + } + + /* package */ void endInteraction() { + mUserInteracting = false; + + updateWidth((int) mNormalWidth); + } + + private void updateHeight(int h) { + if (!isInLayout()) { + final ViewGroup.LayoutParams params = getLayoutParams(); + params.height = h; + setLayoutParams(params); + } + } + + private void updateWidth(int w) { + final ViewGroup.LayoutParams params = getLayoutParams(); + params.width = w; + setLayoutParams(params); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mEqManager.isEqualizerLocked()) { + return false; + } + + final float x = event.getRawX(); + final float y = event.getRawY() - mParentTop; + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + startInteraction(x, y); + break; + + case MotionEvent.ACTION_MOVE: + // Calculate the distance moved + final float dx = x - mLastTouchX; + final float dy = y - mLastTouchY; + + mPosX += dx; + mPosY -= dy; + + // Remember this touch position for the next move event + mLastTouchX = x; + mLastTouchY = y; + + int wy = (int) mParentHeight; + float level = (1 - (mPosY / wy)) * (mEqManager.getMinDB() - mEqManager.getMaxDB()) + - mEqManager.getMinDB(); + + if (DEBUG) Log.d(TAG, "new level: " + level); + if (level < mEqManager.getMinDB()) { + level = mEqManager.getMinDB(); + } else if (level > mEqManager.getMaxDB()) { + level = mEqManager.getMaxDB(); + } + + if (mInitialLevel != level) { + mEqManager.setLevel(getInfo().mIndex, level, false); + } else { + updateHeight(); + } + + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + endInteraction(); + break; + + } + + return true; + } + + public float getPosY() { + return mPosY; + } + + @Override + public void onBandLevelChange(int band, float dB, boolean fromSystem) { + if (getInfo().mIndex != band) { + return; + } + + updateHeight(); + } + + @Override + public void onPresetChanged(int newPresetIndex) { + + } + + @Override + public void onPresetsChanged() { + + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/eq/EqContainerView.java b/src/org/cyanogenmod/audiofx/audiofx/eq/EqContainerView.java new file mode 100644 index 0000000..51b58a6 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/eq/EqContainerView.java @@ -0,0 +1,518 @@ +package com.cyngn.audiofx.eq; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Vibrator; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver; +import android.widget.CheckBox; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.EqualizerManager; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.activity.StateCallbacks; + +import java.util.ArrayList; +import java.util.List; + +public class EqContainerView extends FrameLayout + implements StateCallbacks.EqUpdatedCallback, StateCallbacks.EqControlStateCallback { + + private static final String TAG = EqContainerView.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private int mWidth; + private int mHeight; + private MasterConfigControl mConfig; + private EqualizerManager mEqManager; + private List<EqBandInfo> mBandInfo; + private List<EqBarView> mBarViews; + private List<Integer> mSelectedBands; + + private CheckBox mLockBox; + private ImageView mRenameControl; + private ImageView mRemoveControl; + private ImageView mSaveControl; + private ViewGroup mControls; + private boolean mControlsVisible; + + private boolean mSaveVisible; + private boolean mRemoveVisible; + private boolean mRenameVisible; + private boolean mUnlockVisible; + + private int mSelectedBandColor; + private boolean mFirstLayout = true; + + private Paint mTextPaint; + private Paint mFreqPaint; + private Paint mSelectedFreqPaint; + private Paint mCenterLinePaint; + private Path mDashPath; + + private Handler mHandler; + + private Runnable mVibrateRunnable = new Runnable() { + @Override + public void run() { + Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(30); + } + }; + + private int mPaddingTop; + private int mPaddingBottom; + private int mBarWidth; + private int mBarSeparation; + private int mBarBottomGrabSpacePadding; + + public void stopListening() { + for (EqBarView barView : mBarViews) { + barView.setTag(null); + mConfig.getCallbacks().removeEqUpdatedCallback(barView); + } + mConfig.getCallbacks().removeEqUpdatedCallback(this); + } + + public void startListening() { + for (int i = 0; i < mBandInfo.size(); i++) { + + final EqBarView eqBarView = mBarViews.get(i); + eqBarView.setTag(mBandInfo.get(i)); + mConfig.getCallbacks().addEqUpdatedCallback(eqBarView); + } + mConfig.getCallbacks().addEqUpdatedCallback(this); + } + + public static class EqBandInfo { + public int mIndex; + + public String mFreq; + public String mDb; + public EqBarView mBar; + } + + public EqContainerView(Context context) { + super(context); + init(); + } + + public EqContainerView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public EqContainerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + setLayerType(LAYER_TYPE_HARDWARE, null); + + mHandler = new Handler(); + + final Resources r = getResources(); + + mBarWidth = r.getDimensionPixelSize(R.dimen.eq_bar_width); + mBarSeparation = r.getDimensionPixelSize(R.dimen.separator_width); + + mBarBottomGrabSpacePadding = r.getDimensionPixelSize(R.dimen.eq_bar_bottom_grab_space); + int freqTextSize = r.getDimensionPixelSize(R.dimen.eq_label_text_size); + int selectedBoxTextSize = r.getDimensionPixelSize(R.dimen.eq_selected_box_height); + + int extraTopSpace = r.getDimensionPixelSize(R.dimen.eq_bar_top_padding); + + mPaddingTop = selectedBoxTextSize + extraTopSpace; + mPaddingBottom = selectedBoxTextSize + mBarBottomGrabSpacePadding; + + mConfig = MasterConfigControl.getInstance(mContext); + mEqManager = mConfig.getEqualizerManager(); + + mBarViews = new ArrayList<>(); + mBandInfo = new ArrayList<>(); + mSelectedBands = new ArrayList<>(); + + setWillNotDraw(false); + + mSelectedBandColor = r.getColor(R.color.band_bar_color_selected); + + mTextPaint = new Paint(); + mTextPaint.setAntiAlias(true); + mTextPaint.setColor(Color.WHITE); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setElegantTextHeight(true); + mTextPaint.setTextSize(selectedBoxTextSize); + + mFreqPaint = new Paint(); + mFreqPaint.setAntiAlias(true); + mFreqPaint.setColor(Color.WHITE); + mFreqPaint.setTextAlign(Paint.Align.CENTER); + mFreqPaint.setTextSize(freqTextSize); + + mSelectedFreqPaint = new Paint(mFreqPaint); + mSelectedFreqPaint.setAntiAlias(true); + mSelectedFreqPaint.setTextSize(selectedBoxTextSize); + + mCenterLinePaint = new Paint(); + mCenterLinePaint.setColor(Color.WHITE); + mCenterLinePaint.setAntiAlias(true); + mCenterLinePaint.setPathEffect(new DashPathEffect(new float[]{6, 6}, 0)); + mCenterLinePaint.setStyle(Paint.Style.STROKE); + mCenterLinePaint.setAntiAlias(true); + + getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + generateAndAddBars(); + } + }); + } + + @Override + public boolean hasOverlappingRendering() { + return true; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mControls = (ViewGroup) findViewById(R.id.eq_controls); + + mLockBox = (CheckBox) findViewById(R.id.lock); + mLockBox.setOnCheckedChangeListener(mEqManager.getLockChangeListener()); + + mRenameControl = (ImageView) findViewById(R.id.rename); + mRemoveControl = (ImageView) findViewById(R.id.remove); + mSaveControl = (ImageView) findViewById(R.id.save); + } + + @Override + protected void onAttachedToWindow() { + if (DEBUG) Log.d(TAG, "onAttachedToWindow()"); + super.onAttachedToWindow(); + + mConfig.getCallbacks().addEqControlStateCallback(this); + onPresetChanged(mEqManager.getCurrentPresetIndex()); // update initial state + } + + @Override + protected void onDetachedFromWindow() { + if (DEBUG) Log.d(TAG, "onDetachedFromWindow()"); + mConfig.getCallbacks().removeEqControlStateCallback(this); + super.onDetachedFromWindow(); + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + private void generateAndAddBars() { + if (mFirstLayout) { + mFirstLayout = false; + mBarViews.clear(); + + for (int i = 0; i < mEqManager.getNumBands(); i++) { + final EqBandInfo band = new EqBandInfo(); + band.mIndex = i; + mBandInfo.add(band); + + final EqBarView bar = new EqBarView(mContext); + band.mBar = bar; + bar.setTag(band); + bar.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mEqManager.isEqualizerLocked()) { + return false; + } + switch (event.getActionMasked()) { + + case MotionEvent.ACTION_DOWN: + startBarInteraction(bar); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + stopBarInteraction(bar); + break; + } + + return false; + } + }); + + // set correct initial alpha + if (i % 2 == 0) { + bar.setAlpha(0.6f); + } else { + bar.setAlpha(0.8f); + } + bar.setBackgroundColor(Color.WHITE); + bar.setElevation(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, + getResources().getDisplayMetrics())); + + addView(bar, getFrameParams(i)); + bar.setParentHeight(mHeight, getTop()); + + final float freq = mEqManager.getCenterFreq(i); + String frequencyText = String.format(freq < 1000 ? "%.0f" : "%.0fk", + freq < 1000 ? freq : freq / 1000); + band.mFreq = frequencyText; + mBarViews.add(bar); + } + updateSelectedBands(); + } else { + for (EqBarView barView : mBarViews) { + barView.setParentHeight(mHeight, getTop()); + } + } + } + + public EqBarView startTouchingBarUnder(MotionEvent event) { + EqBarView foundBar = findBar(event.getX(), event.getY(), mBarViews); + if (foundBar != null) { + foundBar.updateHeight(); + + foundBar.startInteraction(event.getRawX(), event.getRawY()); + startBarInteraction(foundBar); + } + return foundBar; + } + + public void startBarInteraction(EqBarView bar) { + setControlsVisible(false, false); + EqBandInfo band = (EqBandInfo) bar.getTag(); + mSelectedBands.add(band.mIndex); + updateSelectedBands(); + AsyncTask.execute(mVibrateRunnable); + } + + public void stopBarInteraction(EqBarView bar) { + EqBandInfo band = (EqBandInfo) bar.getTag(); + mSelectedBands.remove((Integer) band.mIndex); + updateSelectedBands(); + setControlsVisible(mControlsVisible, true); + } + + private EqBarView findBar(float x, float y, List<EqBarView> targets) { + final int count = targets.size(); + for (int i = 0; i < count; i++) { + final EqBarView target = targets.get(i); + if (target.getRight() > x && target.getTop() < y + && target.getBottom() > y && target.getLeft() < x) { + return target; + } + } + return null; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mWidth = w; + mHeight = h - mPaddingTop - mPaddingBottom; + generateAndAddBars(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + //--------------------------------------------------- + + if (mFirstLayout) + return; + + int dashY = bottom - mPaddingBottom - (mHeight / 2); + + final int widthOfBars = (mEqManager.getNumBands() * mBarWidth) + + ((mEqManager.getNumBands() - 1) * mBarSeparation); + final int freeSpace = mWidth - widthOfBars; + + int mCurLeft = (freeSpace / 2); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child instanceof EqBarView) { + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + int l = mCurLeft; + int r = l + mBarWidth; + + mCurLeft += mBarWidth + mBarSeparation; + + if (((EqBarView) child).isUserInteracting()) { + l -= childWidth / 4; + r += childWidth / 4; + } + + final int layoutTop = top + mHeight - childHeight + mPaddingTop; + final int layoutBottom = layoutTop + childHeight + + mPaddingBottom - (mPaddingBottom - mBarBottomGrabSpacePadding); + child.layout(l, layoutTop, r, layoutBottom); + } + } + + if (changed || mDashPath == null) { + mDashPath = new Path(); + mDashPath.reset(); + mDashPath.moveTo(freeSpace / 2, dashY); + mDashPath.lineTo(widthOfBars + (freeSpace / 2), dashY); + } + + mControls.layout( + right - mControls.getMeasuredWidth() - mControls.getPaddingLeft(), + top + mControls.getPaddingTop(), + right - mControls.getPaddingRight(), + top + mControls.getMeasuredHeight() + mControls.getPaddingTop() + + mControls.getPaddingBottom() + ); + } + + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.drawPath(mDashPath, mCenterLinePaint); + + for (int i = 0; i < mBandInfo.size(); i++) { + EqBandInfo info = mBandInfo.get(i); + + final float x = info.mBar.getX() + (info.mBar.getWidth() / 2); + final boolean userInteracting = info.mBar.isUserInteracting(); + if (userInteracting) { + canvas.drawText( + info.mDb, + x, + info.mBar.getY() - (mTextPaint.getTextSize() / 2), + mTextPaint); + } + + Paint drawPaint = userInteracting ? mSelectedFreqPaint : mFreqPaint; + + canvas.drawText(info.mFreq, x, + info.mBar.getBottom() + drawPaint.getTextSize(), + drawPaint); + } + } + + private void updateSelectedBands() { + for (int i = 0; i < mEqManager.getNumBands(); i++) { + EqBandInfo tag = mBandInfo.get(i); + final EqBarView bar = (EqBarView) findViewWithTag(tag); + if (bar != null) { + final ViewPropertyAnimator barAnimation = bar.animate().withLayer(); + if (mSelectedBands.isEmpty()) { + if (i % 2 == 0) { + barAnimation.alpha(0.6f); + } else { + barAnimation.alpha(0.8f); + } + } else if (mSelectedBands.contains(i)) { + barAnimation.alpha(1f); + bar.setBackgroundColor(mSelectedBandColor); + } else { + barAnimation.alpha(0.40f); + } + } + } + } + + private FrameLayout.LayoutParams getFrameParams(int index) { + int width = getResources().getDimensionPixelSize(R.dimen.eq_bar_width); + int height = Math.round((1 - mEqManager.projectY(mEqManager.getLevel(index))) * mHeight); + FrameLayout.LayoutParams ll = new FrameLayout.LayoutParams(width, height); + ll.gravity = Gravity.TOP; + return ll; + } + + @Override + public void onBandLevelChange(int band, float dB, boolean fromSystem) { + if (mFirstLayout) return; + mBandInfo.get(band).mDb = dB != 0 ? String.format("%+1.1f", dB) : "0.0"; + invalidate(); + } + + @Override + public void onPresetChanged(int newPresetIndex) { + updateEqState(); + if (mEqManager.isUserPreset()) { + mLockBox.setChecked(mEqManager.isEqualizerLocked()); + } + } + + @Override + public void updateEqState(boolean saveVisible, boolean removeVisible, + boolean renameVisible, boolean unlockVisible) { + mControlsVisible = mEqManager.isUserPreset() || mEqManager.isCustomPreset(); + mSaveVisible = saveVisible; + mRemoveVisible = removeVisible; + mRenameVisible = renameVisible; + mUnlockVisible = unlockVisible; + updateEqState(); + } + + public void updateEqState() { + setControlsVisible(mControlsVisible && mSelectedBands.isEmpty(), false); + + animateControl(mLockBox, mUnlockVisible); + animateControl(mRemoveControl, mRemoveVisible); + animateControl(mRenameControl, mRenameVisible); + animateControl(mSaveControl, mSaveVisible); + } + + private void animateControl(final View v, boolean visible) { + if (visible) { + v.setVisibility(View.VISIBLE); + v.animate() + .alpha(1f) + .setDuration(350) + .withEndAction(null); + } else { + v.animate() + .alpha(0f) + .setDuration(350) + .withEndAction(new Runnable() { + @Override + public void run() { + v.setVisibility(View.INVISIBLE); + } + }); + } + } + + @Override + public void onPresetsChanged() { + } + + public void setControlsVisible(boolean visible, boolean keepChange) { + if (keepChange) { + mControlsVisible = visible; + } + + if (mControls != null) { + animateControl(mControls, visible); + } + } + +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/eq/EqSwipeController.java b/src/org/cyanogenmod/audiofx/audiofx/eq/EqSwipeController.java new file mode 100644 index 0000000..e96d944 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/eq/EqSwipeController.java @@ -0,0 +1,133 @@ +package com.cyngn.audiofx.eq; + +import android.content.Context; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.EqualizerManager; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.preset.InfiniteViewPager; + +public class EqSwipeController extends LinearLayout { + + /* + * x velocity max for deciding whether to try to grab a bar + */ + private static final int X_VELOCITY_THRESH = 20; + + private static final int MINIMUM_TIME_HOLD_TIME = 100; + + EqContainerView mEq; + InfiniteViewPager mPager; + private VelocityTracker mVelocityTracker = null; + long mDownTime; + EqBarView mBar; + boolean mBarActive; + private ViewGroup mControls; + + private final EqualizerManager mEqManager; + private float mDownPositionX; + private float mDownPositionY; + + public EqSwipeController(Context context, AttributeSet attrs) { + super(context, attrs); + mEqManager = MasterConfigControl.getInstance(context).getEqualizerManager(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mEq = (EqContainerView) findViewById(R.id.eq_container); + mPager = (InfiniteViewPager) findViewById(R.id.pager); + mControls = (ViewGroup) findViewById(R.id.eq_controls); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + + // don't intercept touches over the EQ controls + if (mControls.getRight() > x && mControls.getTop() < y + && mControls.getBottom() > y && mControls.getLeft() < x) { + return false; + } + + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int index = event.getActionIndex(); + int action = event.getActionMasked(); + int pointerId = event.getPointerId(index); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mDownPositionX = event.getRawX(); + mDownPositionY = event.getRawY(); + mDownTime = System.currentTimeMillis(); + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + mVelocityTracker.addMovement(event); + break; + case MotionEvent.ACTION_MOVE: + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + mVelocityTracker.computeCurrentVelocity(1000); + float xVelocity = mVelocityTracker.getXVelocity(pointerId); + float yVelocity = mVelocityTracker.getYVelocity(pointerId); + + final float deltaX = mDownPositionX - event.getRawX(); + final float deltaY = mDownPositionY - event.getRawY(); + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + + final ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); + final int touchSlop = viewConfiguration.getScaledTouchSlop(); + + if (!mBarActive && !mEqManager.isChangingPresets() + && !mEqManager.isEqualizerLocked() + && Math.abs(xVelocity) < X_VELOCITY_THRESH + && System.currentTimeMillis() - mDownTime > MINIMUM_TIME_HOLD_TIME) { + if (distanceSquared < touchSlop * touchSlop) { + mBarActive = true; + mBar = mEq.startTouchingBarUnder(event); + } + } + } + + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + if (mBarActive) { + // reset state? + if (mBar != null) { + mEq.stopBarInteraction(mBar); + mBar.endInteraction(); + } + } + mBar = null; + mBarActive = false; + break; + } + if (mBarActive && mBar != null) { + return mBar.onTouchEvent(event); + } else { + return mPager.onTouchEvent(event); + } + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/eq/EqUtils.java b/src/org/cyanogenmod/audiofx/audiofx/eq/EqUtils.java new file mode 100644 index 0000000..30d546c --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/eq/EqUtils.java @@ -0,0 +1,149 @@ +package com.cyngn.audiofx.eq; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.cyngn.audiofx.Preset; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class EqUtils { + + private static final String TAG = EqUtils.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String DEFAULT_DELIMITER = ";"; + + public static String getZeroedBandsString(int length) { + return getZeroedBandsString(length, DEFAULT_DELIMITER); + } + + public static float[] stringBandsToFloats(String input) { + return stringBandsToFloats(input, DEFAULT_DELIMITER); + } + + public static String floatLevelsToString(float[] levels) { + return floatLevelsToString(levels, DEFAULT_DELIMITER); + } + + public static short[] stringBandsToShorts(String input) { + return stringBandsToShorts(input, DEFAULT_DELIMITER); + } + + public static String shortLevelsToString(short[] levels) { + return shortLevelsToString(levels, DEFAULT_DELIMITER); + } + + public static String getZeroedBandsString(int length, final String delimiter) { + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < length; i++) { + buff.append("0").append(delimiter); + } + buff.deleteCharAt(buff.length() - 1); + return buff.toString(); + } + + public static String floatLevelsToString(float[] levels, final String delimiter) { + // save + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < levels.length; i++) { + builder.append(levels[i]); + builder.append(delimiter); + } + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + + + public static String shortLevelsToString(short[] levels, final String delimiter) { + // save + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < levels.length; i++) { + builder.append(levels[i]); + builder.append(delimiter); + } + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + + public static String intLevelsToString(int[] levels, final String delimiter) { + // save + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < levels.length; i++) { + builder.append(levels[i]); + builder.append(delimiter); + } + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + + public static <T> String levelsToString(T[] levels, final String delimiter) { + // save + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < levels.length; i++) { + builder.append(levels[i]); + builder.append(delimiter); + } + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + + public static short[] stringBandsToShorts(String input, final String delimiter) { + String[] levels = input.split(delimiter); + + short[] equalizerLevels = new short[levels.length]; + for (int i = 0; i < levels.length; i++) { + equalizerLevels[i] = (short) (Float.parseFloat(levels[i])); + } + return equalizerLevels; + } + + + public static float[] stringBandsToFloats(String input, final String delimiter) { + String[] levels = input.split(delimiter); + + float[] equalizerLevels = new float[levels.length]; + for (int i = 0; i < levels.length; i++) { + equalizerLevels[i] = (Float.parseFloat(levels[i])); + } + return equalizerLevels; + } + + public static float[] convertDecibelsToMillibels(float[] decibels) { + if (DEBUG) Log.i(TAG, "++ convertDecibelsToMillibels(" + Arrays.toString(decibels) + ")"); + + float[] newvals = new float[decibels.length]; + for (int i = 0; i < decibels.length; i++) { + newvals[i] = decibels[i] * 100; + } + + + if (DEBUG) Log.i(TAG, "-- convertDecibelsToMillibels(" + Arrays.toString(newvals) + ")"); + return newvals; + } + + public static short[] convertDecibelsToMillibelsInShorts(float[] decibels) { + if (DEBUG) Log.i(TAG, "++ convertDecibelsToMillibels(" + Arrays.toString(decibels) + ")"); + + short[] newvals = new short[decibels.length]; + for (int i = 0; i < decibels.length; i++) { + newvals[i] = (short) (decibels[i] * 100); + } + + + if (DEBUG) Log.i(TAG, "-- convertDecibelsToMillibels(" + Arrays.toString(newvals) + ")"); + return newvals; + } + + public static float[] convertMillibelsToDecibels(float[] millibels) { + if (DEBUG) Log.i(TAG, "++ convertMillibelsToDecibels(" + Arrays.toString(millibels) + ")"); + float[] newvals = new float[millibels.length]; + for (int i = 0; i < millibels.length; i++) { + newvals[i] = millibels[i] / 100; + } + if (DEBUG) Log.i(TAG, "-- convertMillibelsToDecibels(" + Arrays.toString(newvals) + ")"); + return newvals; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/fragment/AudioFxBaseFragment.java b/src/org/cyanogenmod/audiofx/audiofx/fragment/AudioFxBaseFragment.java new file mode 100644 index 0000000..d487ff0 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/fragment/AudioFxBaseFragment.java @@ -0,0 +1,66 @@ +package com.cyngn.audiofx.fragment; + +import android.animation.Animator; +import android.app.Fragment; +import android.os.Bundle; +import android.widget.CompoundButton; +import com.cyngn.audiofx.activity.ActivityMusic; +import com.cyngn.audiofx.activity.MasterConfigControl; + +public class AudioFxBaseFragment extends Fragment { + + MasterConfigControl mConfig; + + AudioFxFragment mFrag; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mFrag = (AudioFxFragment) getParentFragment(); + + mConfig = MasterConfigControl.getInstance(getActivity()); + } + + public int getDisabledColor() { + return mFrag.getDisabledColor(); + } + + public int getCurrentBackgroundColor() { + return mFrag.mCurrentBackgroundColor; + } + + public void animateBackgroundColorTo(Integer colorTo, Animator.AnimatorListener listener, + AudioFxFragment.ColorUpdateListener updateListener) { + if (mFrag != null) { + mFrag.animateBackgroundColorTo(colorTo, listener, updateListener); + } + } + + /** + * Call to change the color and propogate it up to the activity, which will call + * {@link #updateFragmentBackgroundColors(int)} + * + * @param color + */ + public void setBackgroundColor(int color, boolean cancelAnimated) { + if (mFrag != null) { + mFrag.updateBackgroundColors(color, cancelAnimated); + } + } + + /** + * For sub class fragments to override and apply the color + * + * @param color the new color to apply to any colored elements + */ + public void updateFragmentBackgroundColors(int color) { + } + + /** + * For sub class fragments to override when they might need to update their enabled states + */ + public void updateEnabledState() { + + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/fragment/AudioFxFragment.java b/src/org/cyanogenmod/audiofx/audiofx/fragment/AudioFxFragment.java new file mode 100644 index 0000000..2c9c5f5 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/fragment/AudioFxFragment.java @@ -0,0 +1,489 @@ +package com.cyngn.audiofx.fragment; + +import android.animation.Animator; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.AudioDeviceInfo; +import android.os.Bundle; +import android.os.Handler; +import android.util.ArrayMap; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import com.cyngn.audiofx.Compatibility; +import com.cyngn.audiofx.Constants; +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.ActivityMusic; +import com.cyngn.audiofx.activity.EqualizerManager; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.activity.StateCallbacks; +import com.cyngn.audiofx.stats.UserSession; +import com.cyngn.audiofx.widget.InterceptableLinearLayout; + +import java.util.List; +import java.util.Map; + +public class AudioFxFragment extends Fragment implements StateCallbacks.DeviceChangedCallback { + + private static final String TAG = AudioFxFragment.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + public static final String TAG_EQUALIZER = "equalizer"; + public static final String TAG_CONTROLS = "controls"; + + Handler mHandler; + int mCurrentBackgroundColor; + + // whether we are in the middle of animating while switching devices + boolean mDeviceChanging; + + private MenuItem mMenuDevices; + + // current selected index + public int mSelectedPosition = 0; + + EqualizerFragment mEqFragment; + ControlsFragment mControlFragment; + + InterceptableLinearLayout mInterceptLayout; + private ValueAnimator mColorChangeAnimator; + + private int mDisabledColor; + + private MasterConfigControl mConfig; + private EqualizerManager mEqManager; + + private AudioDeviceInfo mSystemDevice; + private AudioDeviceInfo mUserSelection; + + private final Map<MenuItem, AudioDeviceInfo> mMenuItems = new ArrayMap<MenuItem, AudioDeviceInfo>(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mConfig = MasterConfigControl.getInstance(getActivity()); + mEqManager = mConfig.getEqualizerManager(); + + if (savedInstanceState != null) { + int user = savedInstanceState.getInt("user_device"); + mUserSelection = mConfig.getDeviceById(user); + int system = savedInstanceState.getInt("system_device"); + mSystemDevice = mConfig.getDeviceById(system); + } + + mHandler = new Handler(); + mDisabledColor = getResources().getColor(R.color.disabled_eq); + + setHasOptionsMenu(true); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("user_device", mUserSelection == null ? -1 : mUserSelection.getId()); + outState.putInt("system_device", mSystemDevice == null ? -1 : mSystemDevice.getId()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + } + + private boolean showFragments() { + boolean createNewFrags = true; + final FragmentTransaction fragmentTransaction = getChildFragmentManager() + .beginTransaction(); + if (mEqFragment == null) { + mEqFragment = (EqualizerFragment) getChildFragmentManager() + .findFragmentByTag(TAG_EQUALIZER); + + if (mEqFragment != null) { + fragmentTransaction.show(mEqFragment); + } + } + if (mControlFragment == null) { + mControlFragment = (ControlsFragment) getChildFragmentManager() + .findFragmentByTag(TAG_CONTROLS); + if (mControlFragment != null) { + fragmentTransaction.show(mControlFragment); + } + } + + if (mEqFragment != null && mControlFragment != null) { + createNewFrags = false; + } + + fragmentTransaction.commit(); + + return createNewFrags; + } + + @Override + public void onResume() { + + mConfig.getCallbacks().addDeviceChangedCallback(this); + mConfig.bindService(); + mConfig.setAutoBindToService(true); + + updateEnabledState(); + + super.onResume(); + + mCurrentBackgroundColor = !mConfig.isCurrentDeviceEnabled() + ? mDisabledColor + : mEqManager.getAssociatedPresetColorHex( + mEqManager.getCurrentPresetIndex()); + updateBackgroundColors(mCurrentBackgroundColor, false); + + promptIfNotDefault(); + } + + private void promptIfNotDefault() { + final String audioFxPackageName = getActivity().getPackageName(); + + final SharedPreferences musicFxPrefs = Constants.getMusicFxPrefs(getActivity()); + final String defaultPackage = musicFxPrefs.getString(Constants.MUSICFX_DEFAULT_PACKAGE_KEY, + audioFxPackageName); + final boolean notDefault = !defaultPackage.equals(audioFxPackageName); + + if (notDefault) { + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.snack_bar_not_default) + .setNegativeButton(R.string.snack_bar_not_default_not_now, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + getActivity().finish(); + } + }) + .setPositiveButton(R.string.snack_bar_not_default_set, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent updateIntent = new Intent(getActivity(), + Compatibility.Service.class); + updateIntent.putExtra("defPackage", audioFxPackageName); + updateIntent.putExtra("defName", ActivityMusic.class.getName()); + getActivity().startService(updateIntent); + dialog.dismiss(); + } + }) + .setCancelable(false) + .create() + .show(); + } + } + + @Override + public void onPause() { + mConfig.setAutoBindToService(false); + mConfig.getCallbacks().removeDeviceChangedCallback(this); + super.onPause(); + mConfig.unbindService(); + } + + public void updateBackgroundColors(Integer color, boolean cancelAnimated) { + if (cancelAnimated && mColorChangeAnimator != null) { + mColorChangeAnimator.cancel(); + } + mCurrentBackgroundColor = color; + if (mEqFragment != null) { + mEqFragment.updateFragmentBackgroundColors(color); + } + if (mControlFragment != null) { + mControlFragment.updateFragmentBackgroundColors(color); + } + } + + public void updateEnabledState() { + boolean currentDeviceEnabled = mConfig.isCurrentDeviceEnabled(); + if (mEqFragment != null) { + mEqFragment.updateEnabledState(); + } + if (mControlFragment != null) { + mControlFragment.updateEnabledState(); + } + + ((ActivityMusic) getActivity()).setGlobalToggleChecked(currentDeviceEnabled); + + if (mInterceptLayout != null) { + mInterceptLayout.setInterception(!currentDeviceEnabled); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.devices, menu); + mMenuDevices = menu.findItem(R.id.devices); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + mMenuDevices.getSubMenu().clear(); + mMenuItems.clear(); + + final AudioDeviceInfo currentDevice = mConfig.getCurrentDevice(); + + MenuItem selectedItem = null; + + List<AudioDeviceInfo> speakerDevices = mConfig.getConnectedDevices( + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); + if (speakerDevices.size() > 0) { + AudioDeviceInfo ai = speakerDevices.get(0); + int viewId = View.generateViewId(); + MenuItem item = mMenuDevices.getSubMenu().add(R.id.devices, viewId, + Menu.NONE, MasterConfigControl.getDeviceDisplayString(getActivity(), ai)); + item.setIcon(R.drawable.ic_action_dsp_icons_speaker); + mMenuItems.put(item, ai); + selectedItem = item; + } + + List<AudioDeviceInfo> headsetDevices = mConfig.getConnectedDevices( + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET); + if (headsetDevices.size() > 0) { + AudioDeviceInfo ai = headsetDevices.get(0); + int viewId = View.generateViewId(); + MenuItem item = mMenuDevices.getSubMenu().add(R.id.devices, viewId, + Menu.NONE, MasterConfigControl.getDeviceDisplayString(getActivity(), ai)); + item.setIcon(R.drawable.ic_action_dsp_icons_headphones); + mMenuItems.put(item, ai); + if (currentDevice.getId() == ai.getId()) { + selectedItem = item; + } + } + + List<AudioDeviceInfo> lineOutDevices = mConfig.getConnectedDevices( + AudioDeviceInfo.TYPE_LINE_ANALOG, AudioDeviceInfo.TYPE_LINE_DIGITAL); + if (lineOutDevices.size() > 0) { + AudioDeviceInfo ai = lineOutDevices.get(0); + int viewId = View.generateViewId(); + MenuItem item = mMenuDevices.getSubMenu().add(R.id.devices, viewId, + Menu.NONE, MasterConfigControl.getDeviceDisplayString(getActivity(), ai)); + item.setIcon(R.drawable.ic_action_dsp_icons_lineout); + mMenuItems.put(item, ai); + if (currentDevice.getId() == ai.getId()) { + selectedItem = item; + } + } + + List<AudioDeviceInfo> bluetoothDevices = mConfig.getConnectedDevices( + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); + for (AudioDeviceInfo ai : bluetoothDevices) { + int viewId = View.generateViewId(); + MenuItem item = mMenuDevices.getSubMenu().add(R.id.devices, viewId, + Menu.NONE, MasterConfigControl.getDeviceDisplayString(getActivity(), ai)); + item.setIcon(R.drawable.ic_action_dsp_icons_bluetoof); + mMenuItems.put(item, ai); + if (currentDevice.getId() == ai.getId()) { + selectedItem = item; + } + } + + List<AudioDeviceInfo> usbDevices = mConfig.getConnectedDevices( + AudioDeviceInfo.TYPE_USB_ACCESSORY, AudioDeviceInfo.TYPE_USB_DEVICE); + for (AudioDeviceInfo ai : usbDevices) { + int viewId = View.generateViewId(); + MenuItem item = mMenuDevices.getSubMenu().add(R.id.devices, viewId, + Menu.NONE, MasterConfigControl.getDeviceDisplayString(getActivity(), ai)); + item.setIcon(R.drawable.ic_action_device_usb); + mMenuItems.put(item, ai); + if (currentDevice.getId() == ai.getId()) { + selectedItem = item; + } + } + mMenuDevices.getSubMenu().setGroupCheckable(R.id.devices, true, true); + selectedItem.setChecked(true); + mMenuDevices.setIcon(selectedItem.getIcon()); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + AudioDeviceInfo device = mMenuItems.get(item); + + if (device != null) { + UserSession.getInstance().deviceChanged(); + mDeviceChanging = true; + if (item.isCheckable()) { + item.setChecked(!item.isChecked()); + } + mSystemDevice = mConfig.getSystemDevice(); + mUserSelection = device; + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mConfig.setCurrentDevice(mUserSelection, true); + } + }); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + Bundle savedInstanceState) { + if (container == null) { + Log.w(TAG, "container is null."); + // no longer displaying this fragment + return null; + } + + View root = inflater.inflate(mConfig.hasMaxxAudio() + ? R.layout.fragment_audiofx_maxxaudio + : R.layout.fragment_audiofx, container, false); + + final FragmentTransaction fragmentTransaction = getChildFragmentManager() + .beginTransaction(); + + boolean createNewFrags = true; + + if (savedInstanceState != null) { + createNewFrags = showFragments(); + } + + if (createNewFrags) { + fragmentTransaction.add(R.id.equalizer, mEqFragment = new EqualizerFragment(), + TAG_EQUALIZER); + fragmentTransaction.add(R.id.controls, mControlFragment = new ControlsFragment(), + TAG_CONTROLS); + } + + fragmentTransaction.commit(); + + + return root; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + // view was destroyed + final FragmentTransaction fragmentTransaction = getChildFragmentManager() + .beginTransaction(); + + if (mEqFragment != null) { + fragmentTransaction.remove(mEqFragment); + mEqFragment = null; + } + if (mControlFragment != null) { + fragmentTransaction.remove(mControlFragment); + mControlFragment = null; + } + + fragmentTransaction.commitAllowingStateLoss(); + + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mInterceptLayout = (InterceptableLinearLayout) view.findViewById(R.id.interceptable_layout); + } + + public void animateBackgroundColorTo(int colorTo, Animator.AnimatorListener listener, + ColorUpdateListener updateListener) { + if (mColorChangeAnimator != null) { + mColorChangeAnimator.cancel(); + mColorChangeAnimator = null; + } + mColorChangeAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), + mCurrentBackgroundColor, colorTo); + mColorChangeAnimator.setDuration(500); + mColorChangeAnimator.addUpdateListener(updateListener != null ? updateListener + : mColorUpdateListener); + if (listener != null) { + mColorChangeAnimator.addListener(listener); + } + mColorChangeAnimator.start(); + } + + @Override + public void onDeviceChanged(AudioDeviceInfo device, boolean userChange) { + updateEnabledState(); + getActivity().invalidateOptionsMenu(); + } + + public CompoundButton getGlobalSwitch() { + return ((ActivityMusic) getActivity()).getGlobalSwitch(); + } + + @Override + public void onGlobalDeviceToggle(final boolean checked) { + final CompoundButton buttonView = getGlobalSwitch(); + final Animator.AnimatorListener animatorListener = new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + buttonView.setEnabled(false); + } + + @Override + public void onAnimationEnd(Animator animation) { + updateEnabledState(); + buttonView.setEnabled(true); + } + + @Override + public void onAnimationCancel(Animator animation) { + buttonView.setEnabled(true); + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }; + final Integer colorTo = checked + ? mEqManager.getAssociatedPresetColorHex(mEqManager.getCurrentPresetIndex()) + : mDisabledColor; + animateBackgroundColorTo(colorTo, animatorListener, null); + } + + private ValueAnimator.AnimatorUpdateListener mColorUpdateListener + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + updateBackgroundColors((Integer) animation.getAnimatedValue(), false); + } + }; + + public static class ColorUpdateListener implements ValueAnimator.AnimatorUpdateListener { + + final AudioFxBaseFragment mFrag; + + public ColorUpdateListener(AudioFxBaseFragment frag) { + this.mFrag = frag; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mFrag.setBackgroundColor((Integer) animation.getAnimatedValue(), false); + } + } + + public int getDisabledColor() { + return mDisabledColor; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/fragment/ControlsFragment.java b/src/org/cyanogenmod/audiofx/audiofx/fragment/ControlsFragment.java new file mode 100644 index 0000000..02e7077 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/fragment/ControlsFragment.java @@ -0,0 +1,103 @@ +package com.cyngn.audiofx.fragment; + +import android.annotation.Nullable; +import android.media.AudioDeviceInfo; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; + +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.knobs.KnobCommander; +import com.cyngn.audiofx.knobs.KnobContainer; +import com.cyngn.audiofx.stats.UserSession; + +public class ControlsFragment extends AudioFxBaseFragment { + + private static final String TAG = ControlsFragment.class.getSimpleName(); + private static final boolean DEBUG = false; + + KnobCommander mKnobCommander; + KnobContainer mKnobContainer; + CheckBox mMaxxVolumeSwitch; + + private CompoundButton.OnCheckedChangeListener mMaxxVolumeListener + = new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (mConfig.getMaxxVolumeEnabled() != isChecked) { + UserSession.getInstance().maxxVolumeToggled(); + } + mConfig.setMaxxVolumeEnabled(isChecked); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mKnobCommander = KnobCommander.getInstance(getActivity()); + } + + @Override + public void onPause() { + MasterConfigControl.getInstance(getActivity()).getCallbacks().removeDeviceChangedCallback(mKnobContainer); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + MasterConfigControl.getInstance(getActivity()).getCallbacks().addDeviceChangedCallback(mKnobContainer); + } + + @Override + public void updateFragmentBackgroundColors(int color) { + if (mKnobContainer != null) { + mKnobContainer.updateKnobHighlights(color); + } + } + + + public void updateEnabledState() { + final AudioDeviceInfo device = mConfig.getCurrentDevice(); + boolean currentDeviceEnabled = mConfig.isCurrentDeviceEnabled(); + + if (DEBUG) { + Log.d(TAG, "updating with current device: " + device.getType()); + } + + if (mMaxxVolumeSwitch != null) { + mMaxxVolumeSwitch.setChecked(mConfig.getMaxxVolumeEnabled()); + mMaxxVolumeSwitch.setEnabled(currentDeviceEnabled); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(mConfig.hasMaxxAudio() ? R.layout.controls_maxx_audio + : R.layout.controls_generic, container, false); + return root; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mKnobContainer = (KnobContainer) view.findViewById(R.id.knob_container); + mMaxxVolumeSwitch = (CheckBox) view.findViewById(R.id.maxx_volume_switch); + + updateFragmentBackgroundColors(getCurrentBackgroundColor()); + + if (mMaxxVolumeSwitch != null) { + mMaxxVolumeSwitch.setOnCheckedChangeListener(mMaxxVolumeListener); + } + } + + +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/fragment/EqualizerFragment.java b/src/org/cyanogenmod/audiofx/audiofx/fragment/EqualizerFragment.java new file mode 100644 index 0000000..2d958b2 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/fragment/EqualizerFragment.java @@ -0,0 +1,559 @@ +package com.cyngn.audiofx.fragment; + +import android.animation.Animator; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.graphics.drawable.ColorDrawable; +import android.media.AudioDeviceInfo; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.view.ViewPager; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; + +import com.cyngn.audiofx.Preset; +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.EqualizerManager; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.activity.StateCallbacks; +import com.cyngn.audiofx.eq.EqContainerView; +import com.cyngn.audiofx.preset.InfinitePagerAdapter; +import com.cyngn.audiofx.preset.InfiniteViewPager; +import com.cyngn.audiofx.preset.PresetPagerAdapter; +import com.cyngn.audiofx.stats.UserSession; +import com.cyngn.audiofx.viewpagerindicator.CirclePageIndicator; + +import java.util.Arrays; + +public class EqualizerFragment extends AudioFxBaseFragment + implements StateCallbacks.DeviceChangedCallback, StateCallbacks.EqUpdatedCallback { + + private static final String TAG = EqualizerFragment.class.getSimpleName(); + private static final boolean DEBUG = false; + private static final boolean DEBUG_VIEWPAGER = true; + + private final ArgbEvaluator mArgbEval = new ArgbEvaluator(); + + public EqContainerView mEqContainer; + InfiniteViewPager mPresetPager; + CirclePageIndicator mPresetPageIndicator; + PresetPagerAdapter mDataAdapter; + InfinitePagerAdapter mInfiniteAdapter; + int mCurrentRealPage; + + private Handler mHandler; + + // whether we are in the middle of animating while switching devices + boolean mDeviceChanging; + + private ViewPager mFakePager; + + private int mAnimatingToRealPageTarget = -1; + + /* + * this array can hold on to arrays which store preset levels, + * so modifying values in here should only be done with extreme care + */ + private float[] mSelectedPositionBands; + + // current selected index + public int mSelectedPosition = 0; + + private MasterConfigControl mConfig; + private EqualizerManager mEqManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mConfig = MasterConfigControl.getInstance(getActivity()); + mEqManager = mConfig.getEqualizerManager(); + + mHandler = new Handler(); + } + + @Override + public void onPause() { + mEqContainer.stopListening(); + mConfig.getCallbacks().removeDeviceChangedCallback(this); + mConfig.getCallbacks().removeEqUpdatedCallback(this); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + mEqContainer.startListening(); + mConfig.getCallbacks().addEqUpdatedCallback(this); + mConfig.getCallbacks().addDeviceChangedCallback(this); + mPresetPageIndicator.notifyDataSetChanged(); + mDataAdapter.notifyDataSetChanged(); + } + + @Override + public void updateFragmentBackgroundColors(int color) { + if (getActivity() != null && getActivity().getWindow() != null) { + getActivity().getWindow().setBackgroundDrawable(new ColorDrawable(color)); + } + } + + public void jumpToPreset(int index) { + int diff = index - (mCurrentRealPage % mDataAdapter.getCount()); + // double it, short (e.g. 1 hop) distances sometimes bug out?? + diff += mDataAdapter.getCount(); + int newPage = mCurrentRealPage + diff; + mPresetPager.setCurrentItemAbsolute(newPage, false); + } + + private void removeCurrentCustomPreset(boolean showWarning) { + if (showWarning) { + Preset p = mEqManager.getCurrentPreset(); + new AlertDialog.Builder(getActivity()) + .setMessage(String.format(getString( + R.string.remove_custom_preset_warning_message), p.getName())) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + removeCurrentCustomPreset(false); + } + }) + .create() + .show(); + return; + } + + final int currentIndexBeforeRemove = mEqManager.getCurrentPresetIndex(); + if (mEqManager.removePreset(currentIndexBeforeRemove)) { + mInfiniteAdapter.notifyDataSetChanged(); + mDataAdapter.notifyDataSetChanged(); + mPresetPageIndicator.notifyDataSetChanged(); + + jumpToPreset(mSelectedPosition - 1); + } + } + + private void openRenameDialog() { + AlertDialog.Builder renameDialog = new AlertDialog.Builder(getActivity()); + renameDialog.setTitle(R.string.rename); + final EditText newName = new EditText(getActivity()); + newName.setText(mEqManager.getCurrentPreset().getName()); + renameDialog.setView(newName); + renameDialog.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface d, int which) { + mEqManager.renameCurrentPreset(newName.getText().toString()); + final TextView viewWithTag = (TextView) mPresetPager + .findViewWithTag(mEqManager.getCurrentPreset()); + viewWithTag.setText(newName.getText().toString()); + mDataAdapter.notifyDataSetChanged(); + mPresetPager.invalidate(); + } + }); + + renameDialog.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface d, int which) { + } + }); + + // disable ok button if text is empty + final AlertDialog dialog = renameDialog.create(); + newName.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void afterTextChanged(Editable s) { + if (s.length() == 0) { + dialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(false); + } else { + dialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(true); + } + } + }); + + dialog.show(); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.equalizer, container, false); + return root; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mSelectedPositionBands = mEqManager.getPersistedPresetLevels(mEqManager.getCurrentPresetIndex()); + mSelectedPosition = mEqManager.getCurrentPresetIndex(); + + mEqContainer = (EqContainerView) view.findViewById(R.id.eq_container); + mPresetPager = (InfiniteViewPager) view.findViewById(R.id.pager); + mPresetPageIndicator = (CirclePageIndicator) view.findViewById(R.id.indicator); + mFakePager = (ViewPager) view.findViewById(R.id.fake_pager); + + mEqContainer.findViewById(R.id.save).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + + final int newidx = mEqManager.addPresetFromCustom(); + mInfiniteAdapter.notifyDataSetChanged(); + mDataAdapter.notifyDataSetChanged(); + mPresetPageIndicator.notifyDataSetChanged(); + + jumpToPreset(newidx); + } + } + ); + mEqContainer.findViewById(R.id.rename).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mEqManager.isUserPreset()) { + openRenameDialog(); + } + } + } + ); + mEqContainer.findViewById(R.id.remove).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + removeCurrentCustomPreset(true); + } + } + ); + + mDataAdapter = new PresetPagerAdapter(getActivity()); + mInfiniteAdapter = new InfinitePagerAdapter(mDataAdapter); + + mPresetPager.setAdapter(mInfiniteAdapter); + mPresetPager.setOnPageChangeListener(mViewPageChangeListener); + + mFakePager.setAdapter(mDataAdapter); + mCurrentRealPage = mPresetPager.getCurrentItem(); + + mPresetPageIndicator.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + // eat all events + return true; + } + }); + mPresetPageIndicator.setSnap(true); + + mPresetPageIndicator.setViewPager(mFakePager, 0); + mPresetPageIndicator.setCurrentItem(mSelectedPosition); + + mFakePager.setCurrentItem(mSelectedPosition); + mPresetPager.setCurrentItem(mSelectedPosition); + } + + @Override + public void onBandLevelChange(int band, float dB, boolean fromSystem) { + // call backs we get when bands are changing, check if the user is physically touching them + // and set the preset to "custom" and do proper animations. + if (!fromSystem) { // from user + if (!mEqManager.isCustomPreset() // not on custom already + && !mEqManager.isUserPreset() // or not on a user preset + && !mEqManager.isAnimatingToCustom()) { // and animation hasn't started + if (DEBUG) Log.w(TAG, "met conditions to start an animation to custom trigger"); + // view pager is infinite, so we can't set the item to 0. find NEXT 0 + mEqManager.setAnimatingToCustom(true); + + final int newIndex = mEqManager.copyToCustom(); + + mInfiniteAdapter.notifyDataSetChanged(); + mDataAdapter.notifyDataSetChanged(); + mPresetPager.getAdapter().notifyDataSetChanged(); + // do background transition manually as viewpager can't handle this bg change + final Integer colorTo = !mConfig.isCurrentDeviceEnabled() + ? getDisabledColor() + : mEqManager.getAssociatedPresetColorHex(newIndex); + final Animator.AnimatorListener listener = new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + int diff = newIndex - (mCurrentRealPage % mDataAdapter.getCount()); + diff += mDataAdapter.getCount(); + int newPage = mCurrentRealPage + diff; + + mAnimatingToRealPageTarget = newPage; + mPresetPager.setCurrentItemAbsolute(newPage); + } + + @Override + public void onAnimationEnd(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }; + animateBackgroundColorTo(colorTo, listener, null); + + } + mSelectedPositionBands[band] = dB; + } + } + + @Override + public void onPresetChanged(int newPresetIndex) { + } + + @Override + public void onPresetsChanged() { + mDataAdapter.notifyDataSetChanged(); + } + + @Override + public void onDeviceChanged(AudioDeviceInfo device, boolean userChange) { + int diff = mEqManager.getCurrentPresetIndex() - mSelectedPosition; + final boolean samePage = diff == 0; + diff = mDataAdapter.getCount() + diff; + if (DEBUG) { + Log.d(TAG, "diff: " + diff); + } + mCurrentRealPage = mPresetPager.getCurrentItem(); + + if (DEBUG) Log.d(TAG, "mCurrentRealPage Before: " + mCurrentRealPage); + final int newPage = mCurrentRealPage + diff; + if (DEBUG) Log.d(TAG, "mCurrentRealPage After: " + newPage); + + mSelectedPositionBands = mEqManager.getPresetLevels(mSelectedPosition); + final float[] targetBandLevels = mEqManager.getPresetLevels(mEqManager.getCurrentPresetIndex()); + + // do background transition manually as viewpager can't handle this bg change + final Integer colorTo = !mConfig.isCurrentDeviceEnabled() + ? getDisabledColor() + : mEqManager.getAssociatedPresetColorHex(mEqManager.getCurrentPresetIndex()); + + final Animator.AnimatorListener animatorListener = new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + mEqManager.setChangingPresets(true); + + mDeviceChanging = true; + + if (!samePage) { + mPresetPager.setCurrentItemAbsolute(newPage); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + mEqManager.setChangingPresets(false); + + mSelectedPosition = mEqManager.getCurrentPresetIndex(); + mSelectedPositionBands = mEqManager.getPresetLevels(mSelectedPosition); + + mDeviceChanging = false; + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }; + + final AudioFxFragment.ColorUpdateListener animatorUpdateListener + = new AudioFxFragment.ColorUpdateListener(this) { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + super.onAnimationUpdate(animator); + + final int N = mEqManager.getNumBands(); + for (int i = 0; i < N; i++) { // animate bands + float delta = targetBandLevels[i] - mSelectedPositionBands[i]; + float newBandLevel = mSelectedPositionBands[i] + + (delta * animator.getAnimatedFraction()); + //if (DEBUG_VIEWPAGER) Log.d(TAG, i + ", delta: " + delta + ", newBandLevel: " + newBandLevel); + mEqManager.setLevel(i, newBandLevel, true); + } + } + }; + + animateBackgroundColorTo(colorTo, animatorListener, animatorUpdateListener); + } + + @Override + public void onGlobalDeviceToggle(boolean on) { + if (!on) { + mFakePager.setCurrentItem(mFakePager.getCurrentItem(), true); + } + } + + + private ViewPager.OnPageChangeListener mViewPageChangeListener = new ViewPager.OnPageChangeListener() { + + int mState; + float mLastOffset; + boolean mJustGotToCustomAndSettling; + + @Override + public void onPageScrolled(int newPosition, float positionOffset, int positionOffsetPixels) { + if (DEBUG_VIEWPAGER) + Log.i(TAG, "onPageScrolled(" + newPosition + ", " + positionOffset + ", " + + positionOffsetPixels + ")"); + Integer colorFrom; + Integer colorTo; + + if (newPosition == mAnimatingToRealPageTarget && mEqManager.isAnimatingToCustom()) { + if (DEBUG_VIEWPAGER) Log.w(TAG, "settling var set to true"); + mJustGotToCustomAndSettling = true; + mAnimatingToRealPageTarget = -1; + } + + newPosition = newPosition % mDataAdapter.getCount(); + + + if (mEqManager.isAnimatingToCustom() || mDeviceChanging) { + if (DEBUG_VIEWPAGER) + Log.i(TAG, "ignoring onPageScrolled because animating to custom or device is changing"); + return; + } + + int toPos; + if (mLastOffset - positionOffset > 0.8) { // this is needed for flings + //Log.e(TAG, "OFFSET DIFF > 0.8! Setting selected position from: " + mSelectedPosition + " to " + newPosition); + mSelectedPosition = newPosition; + // mSelectedPositionBands will be reset by setPreset() below calling back to onPresetChanged() + + mEqManager.setPreset(mSelectedPosition); + } + + if (newPosition < mSelectedPosition || (newPosition == mDataAdapter.getCount() - 1) + && mSelectedPosition == 0) { + // scrolling left <<<<< + positionOffset = (1 - positionOffset); + //Log.v(TAG, "<<<<<< positionOffset: " + positionOffset + " (last offset: " + mLastOffset + ")"); + toPos = newPosition; + colorTo = mEqManager.getAssociatedPresetColorHex(toPos); + } else { + // scrolling right >>>>> + //Log.v(TAG, ">>>>>>> positionOffset: " + positionOffset + " (last offset: " + mLastOffset + ")"); + toPos = newPosition + 1 % mDataAdapter.getCount(); + if (toPos >= mDataAdapter.getCount()) { + toPos = 0; + } + + colorTo = mEqManager.getAssociatedPresetColorHex(toPos); + } + + if (!mDeviceChanging && mConfig.isCurrentDeviceEnabled()) { + colorFrom = mEqManager.getAssociatedPresetColorHex(mSelectedPosition); + setBackgroundColor((Integer) mArgbEval.evaluate(positionOffset, colorFrom, colorTo), + true); + } + + if (mSelectedPositionBands == null) { + mSelectedPositionBands = mEqManager.getPresetLevels(mSelectedPosition); + } + // get current bands + float[] finalPresetLevels = mEqManager.getPresetLevels(toPos); + + final int N = mEqManager.getNumBands(); + for (int i = 0; i < N; i++) { // animate bands + float delta = finalPresetLevels[i] - mSelectedPositionBands[i]; + float newBandLevel = mSelectedPositionBands[i] + (delta * positionOffset); + //if (DEBUG_VIEWPAGER) Log.d(TAG, i + ", delta: " + delta + ", newBandLevel: " + newBandLevel); + mEqManager.setLevel(i, newBandLevel, true); + } + mLastOffset = positionOffset; + + } + + @Override + public void onPageSelected(int position) { + if (DEBUG_VIEWPAGER) Log.i(TAG, "onPageSelected(" + position + ")"); + mCurrentRealPage = position; + position = position % mDataAdapter.getCount(); + if (DEBUG_VIEWPAGER) Log.e(TAG, "onPageSelected(" + position + ")"); + mFakePager.setCurrentItem(position); + mSelectedPosition = position; + if (!mDeviceChanging) { + mSelectedPositionBands = mEqManager.getPresetLevels(mSelectedPosition); + if (UserSession.getInstance() != null) { + UserSession.getInstance().presetSelected(); + } + } + } + + + @Override + public void onPageScrollStateChanged(int newState) { + mState = newState; + if (mDeviceChanging) { // avoid setting unwanted presets during custom animations + return; + } + if (DEBUG_VIEWPAGER) + Log.w(TAG, "onPageScrollStateChanged(" + stateToString(newState) + ")"); + + if (mJustGotToCustomAndSettling && mState == ViewPager.SCROLL_STATE_IDLE) { + if (DEBUG_VIEWPAGER) + Log.w(TAG, "onPageScrollChanged() setting animating to custom = false"); + mJustGotToCustomAndSettling = false; + mEqManager.setChangingPresets(false); + mEqManager.setAnimatingToCustom(false); + } else { + if (mState == ViewPager.SCROLL_STATE_IDLE) { + animateBackgroundColorTo(!mConfig.isCurrentDeviceEnabled() + ? getDisabledColor() + : mEqManager.getAssociatedPresetColorHex(mSelectedPosition), + null, null); + + mEqManager.setChangingPresets(false); + mEqManager.setPreset(mSelectedPosition); + + } else { + // not idle + mEqManager.setChangingPresets(true); + } + } + } + + private String stateToString(int state) { + switch (state) { + case 0: + return "STATE_IDLE"; + case 1: + return "STATE_DRAGGING"; + case 2: + return "STATE_SETTLING"; + default: + return "STATE_WUT"; + } + } + + }; +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/knobs/KnobCommander.java b/src/org/cyanogenmod/audiofx/audiofx/knobs/KnobCommander.java new file mode 100644 index 0000000..b045c16 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/knobs/KnobCommander.java @@ -0,0 +1,177 @@ +package com.cyngn.audiofx.knobs; + +import android.content.Context; +import com.cyngn.audiofx.Constants; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.service.AudioFxService; + +public class KnobCommander { + + public static final int KNOB_TREBLE = 0; + public static final int KNOB_BASS = 1; + public static final int KNOB_VIRTUALIZER = 2; + + private static KnobCommander sInstance; + + private Context mContext; + private MasterConfigControl mConfig; + + private KnobCommander(Context context) { + mContext = context; + mConfig = MasterConfigControl.getInstance(mContext); + } + + public static KnobCommander getInstance(Context context) { + if (sInstance == null) { + sInstance = new KnobCommander(context); + } + return sInstance; + } + + public RadialKnob.OnKnobChangeListener getRadialKnobCallback(int whichKnob) { + switch (whichKnob) { + case KNOB_TREBLE: return mTrebleKnobCallback; + case KNOB_BASS: return mBassKnobCallback; + case KNOB_VIRTUALIZER: return mVirtualizerCallback; + default: return null; + } + } + + public void updateTrebleKnob(RadialKnob trebleKnob, boolean enabled) { + if (trebleKnob != null) { + trebleKnob.setValue(getTrebleStrength()); + trebleKnob.setOn(isTrebleEffectEnabled()); + trebleKnob.setEnabled(enabled); + } + } + + public void updateBassKnob(RadialKnob bassKnob, boolean enabled) { + if (bassKnob != null) { + bassKnob.setValue(getBassStrength()); + bassKnob.setOn(isBassEffectEnabled()); + bassKnob.setEnabled(enabled); + } + } + + public void updateVirtualizerKnob(RadialKnob virtualizerKnob, boolean enabled) { + if (virtualizerKnob != null) { + virtualizerKnob.setValue(getVirtualizerStrength()); + virtualizerKnob.setOn(isVirtualizerEffectEnabled()); + virtualizerKnob.setEnabled(enabled); + } + } + + public boolean hasBassBoost() { + return mConfig.hasBassBoost(); + } + + public boolean hasTreble() { + return mConfig.hasMaxxAudio() || mConfig.hasDts(); + } + + public boolean hasVirtualizer() { + return mConfig.hasVirtualizer(); + } + + public boolean isBassEffectEnabled() { + return mConfig.getPrefs().getBoolean(Constants.DEVICE_AUDIOFX_BASS_ENABLE, false); + } + + public boolean isTrebleEffectEnabled() { + return mConfig.getPrefs().getBoolean(Constants.DEVICE_AUDIOFX_TREBLE_ENABLE, false); + } + + public boolean isVirtualizerEffectEnabled() { + return mConfig.getPrefs().getBoolean(Constants.DEVICE_AUDIOFX_VIRTUALIZER_ENABLE, false); + } + + public int getVirtualizerStrength() { + return Integer.valueOf(mConfig.getPrefs().getString(Constants.DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH, "0")) / 10; + } + + public int getBassStrength() { + return Integer.valueOf(mConfig.getPrefs().getString(Constants.DEVICE_AUDIOFX_BASS_STRENGTH, "0")) / 10; + } + + public int getTrebleStrength() { + return Integer.valueOf(mConfig.getPrefs().getString(Constants.DEVICE_AUDIOFX_TREBLE_STRENGTH, "0")); + } + + public void setTrebleEnabled(boolean on) { + mConfig.getPrefs().edit().putBoolean(Constants.DEVICE_AUDIOFX_TREBLE_ENABLE, on).apply(); + mConfig.updateService(AudioFxService.TREBLE_BOOST_CHANGED); + } + + public void setTrebleStrength(int value) { + // set parameter and state + mConfig.getPrefs().edit().putString(Constants.DEVICE_AUDIOFX_TREBLE_STRENGTH, String.valueOf(value)).apply(); + mConfig.updateService(AudioFxService.TREBLE_BOOST_CHANGED); + } + + public void setBassEnabled(boolean on) { + mConfig.getPrefs().edit().putBoolean(Constants.DEVICE_AUDIOFX_BASS_ENABLE, on).apply(); + mConfig.updateService(AudioFxService.BASS_BOOST_CHANGED); + } + + public void setBassStrength(int value) { + // set parameter and state + mConfig.getPrefs().edit().putString(Constants.DEVICE_AUDIOFX_BASS_STRENGTH, String.valueOf(value * 10)).apply(); + mConfig.updateService(AudioFxService.BASS_BOOST_CHANGED); + } + + public void setVirtualizerEnabled(boolean on) { + mConfig.getPrefs().edit().putBoolean(Constants.DEVICE_AUDIOFX_VIRTUALIZER_ENABLE, on).apply(); + mConfig.updateService(AudioFxService.VIRTUALIZER_CHANGED); + } + + public void setVirtualiserStrength(int value) { + // set parameter and state + mConfig.getPrefs().edit().putString(Constants.DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH, String.valueOf(value * 10)).apply(); + mConfig.updateService(AudioFxService.VIRTUALIZER_CHANGED); + } + + private RadialKnob.OnKnobChangeListener mTrebleKnobCallback = new RadialKnob.OnKnobChangeListener() { + @Override + public void onValueChanged(RadialKnob knob, int value, boolean fromUser) { + if (fromUser) { + setTrebleStrength(value); + } + } + + @Override + public boolean onSwitchChanged(RadialKnob knob, boolean on) { + setTrebleEnabled(on); + return true; + } + }; + + private RadialKnob.OnKnobChangeListener mBassKnobCallback = new RadialKnob.OnKnobChangeListener() { + @Override + public void onValueChanged(RadialKnob knob, int value, boolean fromUser) { + if (fromUser) { + setBassStrength(value); + } + } + + @Override + public boolean onSwitchChanged(RadialKnob knob, boolean on) { + setBassEnabled(on); + return true; + } + }; + + private RadialKnob.OnKnobChangeListener mVirtualizerCallback = new RadialKnob.OnKnobChangeListener() { + @Override + public void onValueChanged(RadialKnob knob, int value, boolean fromUser) { + if (fromUser) { + setVirtualiserStrength(value); + } + } + + @Override + public boolean onSwitchChanged(RadialKnob knob, boolean on) { + setVirtualizerEnabled(on); + return true; + } + }; +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/knobs/KnobContainer.java b/src/org/cyanogenmod/audiofx/audiofx/knobs/KnobContainer.java new file mode 100644 index 0000000..91e811b --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/knobs/KnobContainer.java @@ -0,0 +1,380 @@ +/* + * 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. + */ +package com.cyngn.audiofx.knobs; + +import android.content.Context; +import android.content.res.Configuration; +import android.media.AudioDeviceInfo; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.activity.StateCallbacks; + +public class KnobContainer extends LinearLayout + implements StateCallbacks.DeviceChangedCallback { + + private static final String TAG = KnobContainer.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final int NOTIFY_DISABLE_DELAY = 5000; + + private static final int MSG_EXPAND = 0; + private static final int MSG_CONTRACT = 1; + + private ViewGroup mTrebleContainer; + private ViewGroup mBassContainer; + private ViewGroup mVirtualizerContainer; + private RadialKnob mTrebleKnob; + private RadialKnob mBassKnob; + private RadialKnob mVirtualizerKnob; + + private H mHandler; + + private KnobCommander mKnobCommander; + + private long mLastDisabledNotifyTime = -1; + + public KnobContainer(Context context) { + super(context); + } + + public KnobContainer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public KnobContainer(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private void init() { + mKnobCommander = KnobCommander.getInstance(mContext); + mHandler = new H(); + + if (!MasterConfigControl.getInstance(mContext).hasMaxxAudio()) { + // we must add the proper knobs dynamically. + if (mKnobCommander.hasBassBoost()) { + mBassContainer = addKnob(KnobCommander.KNOB_BASS); + } + if (mKnobCommander.hasTreble()) { + mTrebleContainer = addKnob(KnobCommander.KNOB_TREBLE); + } + if (mKnobCommander.hasVirtualizer()) { + mVirtualizerContainer = addKnob(KnobCommander.KNOB_VIRTUALIZER); + } + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + init(); + + if (DEBUG) Log.d(TAG, "onFinishInflate()"); + + OnTouchListener knobTouchListener = new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + Message message; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + message = mHandler.obtainMessage(MSG_EXPAND, v.getTag()); + mHandler.sendMessageDelayed(message, 0); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mHandler.removeMessages(MSG_EXPAND); + message = mHandler.obtainMessage(MSG_CONTRACT, v.getTag()); + mHandler.sendMessageDelayed(message, 10); + break; + } + if (!v.isEnabled()) { + notifyDisabled(); + return true; + } + return false; + } + }; + + if (MasterConfigControl.getInstance(getContext()).hasMaxxAudio()) { + mVirtualizerContainer = (ViewGroup) findViewById(R.id.virtualizer_knob_container); + mBassContainer = (ViewGroup) findViewById(R.id.bass_knob_container); + mTrebleContainer = (ViewGroup) findViewById(R.id.treble_knob_container); + } + + if (mTrebleContainer != null) { + mTrebleKnob = (RadialKnob) mTrebleContainer.findViewById(R.id.knob); + mTrebleKnob.setTag(new KnobInfo(KnobCommander.KNOB_TREBLE, mTrebleKnob, + mTrebleContainer.findViewById(R.id.label))); + mTrebleKnob.setOnTouchListener(knobTouchListener); + mTrebleKnob.setOnKnobChangeListener( + KnobCommander.getInstance(getContext()).getRadialKnobCallback( + KnobCommander.KNOB_TREBLE + ) + ); + mTrebleKnob.setMax(100); + } + if (mBassContainer != null) { + mBassKnob = (RadialKnob) mBassContainer.findViewById(R.id.knob); + mBassKnob.setTag(new KnobInfo(KnobCommander.KNOB_BASS, mBassKnob, mBassContainer.findViewById(R.id.label))); + mBassKnob.setOnTouchListener(knobTouchListener); + mBassKnob.setOnKnobChangeListener( + KnobCommander.getInstance(getContext()).getRadialKnobCallback( + KnobCommander.KNOB_BASS + ) + ); + mBassKnob.setMax(100); + + + } + if (mVirtualizerContainer != null) { + mVirtualizerKnob = (RadialKnob) mVirtualizerContainer.findViewById(R.id.knob); + mVirtualizerKnob.setTag(new KnobInfo(KnobCommander.KNOB_VIRTUALIZER, mVirtualizerKnob, + mVirtualizerContainer.findViewById(R.id.label))); + mVirtualizerKnob.setOnTouchListener(knobTouchListener); + mVirtualizerKnob.setOnKnobChangeListener( + KnobCommander.getInstance(getContext()).getRadialKnobCallback( + KnobCommander.KNOB_VIRTUALIZER + ) + ); + mVirtualizerKnob.setMax(100); + } + updateKnobs(MasterConfigControl.getInstance(mContext).getCurrentDevice()); + + if (!MasterConfigControl.getInstance(mContext).hasMaxxAudio()) { + setLayoutTransition(null); + } + } + + private ViewGroup addKnob(int whichKnob) { + ViewGroup knobContainer = (ViewGroup) LayoutInflater.from(mContext) + .inflate(R.layout.generic_knob_control, this, false); + TextView label = (TextView) knobContainer.findViewById(R.id.label); + + int newContainerId = 0; + int knobLabelRes = 0; + switch (whichKnob) { + case KnobCommander.KNOB_BASS: + newContainerId = R.id.bass_knob_container; + knobLabelRes = R.string.bass; + break; + + case KnobCommander.KNOB_TREBLE: + newContainerId = R.id.treble_knob_container; + knobLabelRes = R.string.treble; + break; + + case KnobCommander.KNOB_VIRTUALIZER: + newContainerId = R.id.virtualizer_knob_container; + knobLabelRes = R.string.virtualizer; + break; + + default: + return null; + } + + knobContainer.setId(newContainerId); + label.setText(knobLabelRes); + + addView(knobContainer, getKnobParams()); + return knobContainer; + } + + private LayoutParams getKnobParams() { + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } else { + return new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1); + } + } + + public void setKnobVisible(int knob, boolean visible) { + final int newMode = visible ? View.VISIBLE : View.GONE; + ViewGroup v = null; + switch (knob) { + case KnobCommander.KNOB_VIRTUALIZER: + v = mVirtualizerContainer; + break; + case KnobCommander.KNOB_BASS: + v = mBassContainer; + break; + case KnobCommander.KNOB_TREBLE: + v = mTrebleContainer; + break; + } + if (v == null && visible) { + throw new UnsupportedOperationException("no knob container for knob: " + knob); + } + + if (newMode == v.getVisibility()) { + return; + } + Log.d(TAG, "setKnobVisible() knob=" + knob + " visible=" + visible); + v.setVisibility(newMode); + + // only used on maxx audio layout + if (MasterConfigControl.getInstance(mContext).hasMaxxAudio()) { + /* ensure spacing looks ok! + * + * it goes like, Space, knob layout, Space, knob layout, Space, etc..... + * starting with the first knob (skipping the first space), ensure the pairs have the + * same visibility so there's no extra space at the end. + */ + for (int i = 1; i < getChildCount() - 1; i += 2) { + View layout = getChildAt(i); + View space = getChildAt(i + 1); + if (space.getVisibility() != layout.getVisibility()) { + space.setVisibility(layout.getVisibility()); + } + } + } + } + + public void updateKnobHighlights(int color) { + if (mTrebleKnob != null) { + mTrebleKnob.setHighlightColor(color); + } + if (mBassKnob != null) { + mBassKnob.setHighlightColor(color); + } + if (mVirtualizerKnob != null) { + mVirtualizerKnob.setHighlightColor(color); + } + } + + private void notifyDisabled() { + final long now = System.currentTimeMillis(); + if (mLastDisabledNotifyTime == -1 || now - mLastDisabledNotifyTime > NOTIFY_DISABLE_DELAY) { + mLastDisabledNotifyTime = now; + Toast.makeText(mContext, R.string.effect_unavalable_for_speaker, + Toast.LENGTH_SHORT).show(); + } + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + private void resize(RadialKnob knob, View label, boolean makeBig) { + if (knob.isEnabled()) { + label.animate() + .alpha(makeBig ? 0 : 1) + .setInterpolator(new AccelerateInterpolator()) + .setDuration(100); + + /* + if (makeBig) { + ResizeAnimation resizeAnimation = new ResizeAnimation(this); + resizeAnimation.setHeightParams(getHeight(), mExpandedHeight); + resizeAnimation.setDuration(100); + startAnimation(resizeAnimation); + } else { + ResizeAnimation resizeAnimation = new ResizeAnimation(this); + resizeAnimation.setHeightParams(getHeight(), mRegularHeight); + resizeAnimation.setDuration(100); + startAnimation(resizeAnimation); + } + */ + knob.resize(makeBig); + } + } + + @Override + public void onDeviceChanged(AudioDeviceInfo device, boolean userChange) { + if (device != null) { + updateKnobs(device); + } + } + + @Override + public void onGlobalDeviceToggle(boolean on) { + + } + + private void updateKnobs(AudioDeviceInfo device) { + if (device == null) { + return; + } + final boolean speaker = device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; + final boolean maxxAudio = MasterConfigControl.getInstance(mContext).hasMaxxAudio(); + final boolean dts = MasterConfigControl.getInstance(mContext).hasDts(); + final boolean effectsEnabled = !speaker || maxxAudio || dts; + + mKnobCommander.updateTrebleKnob(mTrebleKnob, effectsEnabled); + mKnobCommander.updateBassKnob(mBassKnob, effectsEnabled); + mKnobCommander.updateVirtualizerKnob(mVirtualizerKnob, effectsEnabled); + if (maxxAudio) { + // speaker? hide virtualizer + setKnobVisible(KnobCommander.KNOB_VIRTUALIZER, !speaker); + } else if (dts) { + // same for DTS + setKnobVisible(KnobCommander.KNOB_VIRTUALIZER, !speaker); + } else { + setKnobVisible(KnobCommander.KNOB_VIRTUALIZER, true); + } + } + + public static class KnobInfo { + int whichKnob; + RadialKnob knob; + View label; + + public KnobInfo(int whichKnob, RadialKnob knob, View label) { + this.knob = knob; + this.label = label; + this.whichKnob = whichKnob; + } + } + + private class H extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_EXPAND: + case MSG_CONTRACT: + RadialKnob knob = ((KnobInfo) msg.obj).knob; + View label = ((KnobInfo) msg.obj).label; + boolean expand = msg.what == MSG_EXPAND; + resize(knob, label, expand); + break; + } + } + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/knobs/RadialKnob.java b/src/org/cyanogenmod/audiofx/audiofx/knobs/RadialKnob.java new file mode 100644 index 0000000..0e4cbb2 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/knobs/RadialKnob.java @@ -0,0 +1,570 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2015, The CyanogenMod Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.cyngn.audiofx.knobs; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Vibrator; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.widget.Toast; +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.stats.UserSession; + +public class RadialKnob extends View { + + private static final String TAG = RadialKnob.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + public static final float REGULAR_SCALE = 0.8f; + + public static final float TOUCHING_SCALE = 1f; + private static final int DO_NOT_VIBRATE_THRESHOLD = 100; + + private static final int DEGREE_OFFSET = -225; + private static final int START_ANGLE = 360 + DEGREE_OFFSET; + private static final int MAX_DEGREES = 270; + + private final Paint mPaint, mTextPaint; + + ValueAnimator mAnimator; + float mOffProgress; + boolean mAnimating = false; + long mDownTime; + long mUpTime; + private OnKnobChangeListener mOnKnobChangeListener = null; + private float mProgress = 0.0f; + private float mTouchProgress = 0.0f; + private int mMax = 100; + private boolean mOn = false; + private boolean mEnabled = true; + private float mLastX; + private float mLastY; + private boolean mMoved; + private int mWidth = 0; + private RectF mRectF, mOuterRect = new RectF(), mInnerRect = new RectF(); + private float mLastAngle; + private Long mLastVibrateTime; + private int mHighlightColor; + private int mBackgroundArcColor; + private int mBackgroundArcColorDisabled; + private int mRectPadding; + private int mStrokeWidth; + private float mHandleWidth; // little square indicator where user touches + private float mTextOffset; + + Path mPath = new Path(); + PathMeasure mPathMeasure = new PathMeasure(); + float[] mTmp = new float[2]; + float mStartX, mStopX, mStartY, mStopY; + + public RadialKnob(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + Resources res = getResources(); + mBackgroundArcColor = res.getColor(R.color.radial_knob_arc_bg); + mBackgroundArcColorDisabled = res.getColor(R.color.radial_knob_arc_bg_disabled); + mHighlightColor = res.getColor(R.color.highlight); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setElegantTextHeight(true); + mTextPaint.setFakeBoldText(true); + mTextPaint.setTextSize(res.getDimension(R.dimen.radial_text_size)); + mTextPaint.setColor(Color.LTGRAY); + + mTextOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, + getResources().getDisplayMetrics()); + + mHandleWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, + getResources().getDisplayMetrics()); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(mHighlightColor); + mPaint.setStrokeWidth(mStrokeWidth = res.getDimensionPixelSize(R.dimen.radial_knob_stroke)); + mPaint.setStrokeCap(Paint.Cap.BUTT); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setShadowLayer(2, 1, -2, getResources().getColor(R.color.black)); + + setScaleX(REGULAR_SCALE); + setScaleY(REGULAR_SCALE); + + mRectPadding = res.getDimensionPixelSize(R.dimen.radial_rect_padding); + invalidate(); + } + + public RadialKnob(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RadialKnob(Context context) { + this(context, null); + } + + public void setValue(int value) { + if (mMax != 0) { + setProgress(((float) value) / mMax); + mTouchProgress = mProgress; + mLastAngle = mProgress * MAX_DEGREES; + } + } + + public void setProgress(float progress, boolean fromUser) { + if (progress > 1.0f) { + progress = 1.0f; + } + if (progress < 0.0f) { + progress = 0.0f; + } + + mProgress = progress; + + invalidate(); + + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onValueChanged(this, (int) (progress * mMax), fromUser); + } + } + + public void setMax(int max) { + mMax = max; + } + + public float getProgress() { + return mProgress; + } + + public void setProgress(float progress) { + setProgress(progress, false); + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public void setEnabled(boolean enabled) { + mEnabled = enabled; + if (enabled) { + setOn(mOn); + } + invalidate(); + } + + public void setOn(final boolean on) { + mOn = on; + if (mAnimator != null) { + mAnimator.cancel(); + } + invalidate(); + } + + public void setHighlightColor(int color) { + mPaint.setColor(mHighlightColor = color); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + mPaint.setStrokeWidth(mStrokeWidth); + + mPaint.setColor(mEnabled ? mBackgroundArcColor : mBackgroundArcColorDisabled); + canvas.drawArc(mRectF, START_ANGLE, MAX_DEGREES, false, mPaint); + + final float sweepAngle = mEnabled ? mProgress * MAX_DEGREES : 0; + if (mOn) { + mPaint.setColor(mHighlightColor); + canvas.drawArc(mRectF, START_ANGLE, sweepAngle, false, mPaint); + } + + final float indicatorSweepAngle = Math.max(1f, sweepAngle); + + // render the indicator + mPath.reset(); + mPath.arcTo(mInnerRect, START_ANGLE, indicatorSweepAngle, true); + + mPathMeasure.setPath(mPath, false); + mPathMeasure.getPosTan(mPathMeasure.getLength(), mTmp, null); + + mStartX = mTmp[0]; + mStartY = mTmp[1]; + + mPath.reset(); + mPath.arcTo(mOuterRect, START_ANGLE, indicatorSweepAngle, true); + + mPathMeasure.setPath(mPath, false); + mPathMeasure.getPosTan(mPathMeasure.getLength(), mTmp, null); + + mStopX = mTmp[0]; + mStopY = mTmp[1]; + + mPaint.setStrokeWidth(mHandleWidth); + mPaint.setColor(Color.WHITE); + canvas.drawLine(mStartX, mStartY, mStopX, mStopY, mPaint); + + canvas.drawText(getProgressText(), + mOuterRect.centerX(), + mOuterRect.centerY() + (mTextPaint.getTextSize() / 2.f) - mTextOffset, + mTextPaint); + } + + private String getProgressText() { + if (mEnabled) { + return ((int) (mProgress * 100)) + "%"; + } else { + return "--"; + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldW, int oldH) { + super.onSizeChanged(w, h, oldW, oldH); + + int size = w > h ? h : w; + mWidth = size; + int diff; + if (w > h) { + diff = (w - h) / 2; + mRectF = new RectF(mRectPadding + diff, mRectPadding, + w - mRectPadding - diff, h - mRectPadding); + } else { + diff = (h - w) / 2; + mRectF = new RectF(mRectPadding, mRectPadding + diff, + w - mRectPadding, h - mRectPadding - diff); + } + mOuterRect.set(mRectF); + mOuterRect.inset(-mRectPadding, -mRectPadding); + mInnerRect.set(mRectF); + mInnerRect.inset(mRectPadding, mRectPadding); + } + + private boolean isUserSelected() { + return getScaleX() == TOUCHING_SCALE && getScaleY() == TOUCHING_SCALE; + } + + private void animateTo(float progress) { + if (DEBUG) Log.w(TAG, "animateTo(" + progress + ")"); + if (mAnimator != null) { + mAnimator.cancel(); + } + mAnimator = ValueAnimator.ofFloat(mProgress, progress); + mAnimator.setDuration(100); + mAnimator.setInterpolator(new AccelerateInterpolator()); + mAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + mAnimating = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + mAnimating = false; + postInvalidate(); + } + + @Override + public void onAnimationCancel(Animator animation) { + mAnimating = false; + postInvalidate(); + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }); + mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float progress = (Float) animation.getAnimatedValue(); + mProgress = progress; + mLastAngle = mProgress * MAX_DEGREES; + if (DEBUG) Log.i(TAG, "onAnimationUpdate(): mProgress: " + + mProgress + ", mLastAngle: " + mLastAngle); + + setProgress(mProgress); + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onValueChanged(RadialKnob.this, + (int) (progress * mMax), true); + } + postInvalidate(); + } + }); + mAnimator.start(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + + if (!mEnabled) { + return false; + } + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mDownTime = System.currentTimeMillis(); + mOffProgress = 0; + + getParent().requestDisallowInterceptTouchEvent(true); + vibrate(); + mLastX = event.getX(); + mLastY = event.getY(); + break; + case MotionEvent.ACTION_MOVE: + // we can be animating while moving + if (mAnimating) { + return true; + } + final float center = getWidth() / 2; + final float radius = (center / 2) - (mRectPadding * 2); + + final boolean inDeadzone = inCircle(x, y, center, center, radius); + final boolean inOuterCircle = inCircle(x, y, center, center, radius + 70); + if (DEBUG) + Log.d(TAG, "inOuterCircle: " + inOuterCircle + ", inDeadzone: " + inDeadzone); + final float delta = getDelta(x, y); + final float angle = angleWithOffset(x, y, DEGREE_OFFSET); + + if (mOn) { + if (isUserSelected() && (!inDeadzone)) { + float angleDiff = Math.abs(mLastAngle - angle); + if (mProgress == 1 && angle < (MAX_DEGREES / 2)) { + // oh jeez. no jumping from 100! + return true; + } + if (angleDiff < 90) { + // jump! + //Log.w(TAG, "using angle"); + mLastAngle = angle; + mTouchProgress = angle / MAX_DEGREES; + mMoved = true; + if (DEBUG) Log.v(TAG, "ANGLE setProgress: " + mTouchProgress); + setProgress(mTouchProgress, true); + } else if (angle > 0 && angle < MAX_DEGREES) { + if (DEBUG) Log.v(TAG, "ANGLE animateTo: " + angle); + mMoved = true; + animateTo(angle / MAX_DEGREES); + } + } + // if it's less than one degree, turn it off + // 1% ~= 2.7 degrees, pick something slightly higher + if (mTouchProgress < (2.71f / MAX_DEGREES) && mOn && mMoved) { + mTouchProgress = (2.71f / MAX_DEGREES); + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onSwitchChanged(this, !mOn); + } + setOn(!mOn); + } + } else { + // off + if (isUserSelected() && (!inDeadzone)) { + if (delta > 0) { + mOffProgress += delta; + } else if (angle > 90) { + mOffProgress = 0; + } + if (DEBUG) + Log.d(TAG, "OFF, touching angle: " + angle + + ", mOffProgress: " + mOffProgress + ", delta " + delta); + // we want at least 1%, how many degrees = 1%? + a little padding + final float onePercentInDegrees = (MAX_DEGREES / 100) + 1f; + if (mOffProgress > 15 && angle < MAX_DEGREES + && angle >= onePercentInDegrees) { + if (DEBUG) Log.w(TAG, "delta: " + delta); + if (angle <= MAX_DEGREES) { + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onSwitchChanged(this, !mOn); + } + + setOn(!mOn); + if (angle > 30) { + animateTo(angle / MAX_DEGREES); + } else { + setProgress(angle / MAX_DEGREES, true); + } + mLastAngle = angle; + mMoved = false; + } else { + if (DEBUG) Log.w(TAG, "off, angle > 300, ignoring"); + } + } + } + mLastX = x; + mLastY = y; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mUpTime = System.currentTimeMillis(); + final float finalAngle = angleWithOffset(x, y, DEGREE_OFFSET); + if (DEBUG) Log.d(TAG, "angle at death: " + finalAngle); + if (mUpTime - mDownTime < 100 && mMoved && finalAngle < MAX_DEGREES) { + if (mOn) { + animateTo(finalAngle / MAX_DEGREES); + } else { + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onSwitchChanged(this, !mOn); + } + + setOn(!mOn); + } + } + if (mMoved) { + UserSession.getInstance() + .knobOptionsAdjusted(((KnobContainer.KnobInfo)getTag()).whichKnob); + } + mLastX = -1; + mLastY = -1; + mOffProgress = 0; + mMoved = false; + break; + default: + break; + } + return true; + } + + private void vibrate() { + if (mLastVibrateTime == null || System.currentTimeMillis() - mLastVibrateTime + > DO_NOT_VIBRATE_THRESHOLD) { + Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + vibrator.vibrate(40); + mLastVibrateTime = System.currentTimeMillis(); + } + } + + public void resize(boolean selected) { + if (!mEnabled) { + return; + } + if (selected) { + animate() + .scaleY(RadialKnob.TOUCHING_SCALE) + .scaleX(RadialKnob.TOUCHING_SCALE) + .setDuration(100); + } else { + animate() + .scaleY(RadialKnob.REGULAR_SCALE) + .scaleX(RadialKnob.REGULAR_SCALE) + .setDuration(100); + } + } + + 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; + } + + private float angleWithOffset(float x, float y, int degreeOffset) { + float angle = angle(x, y); + if (angle > 180) { + angle += degreeOffset; + } else { + angle += (360 + degreeOffset); + } + return angle; + } + + + private static boolean inCircle(float x, float y, float circleCenterX, float circleCenterY, + float circleRadius) { + double dx = Math.pow(x - circleCenterX, 2); + double dy = Math.pow(y - circleCenterY, 2); + + if ((dx + dy) < Math.pow(circleRadius, 2)) { + return true; + } else { + return false; + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public void setOnKnobChangeListener(OnKnobChangeListener l) { + mOnKnobChangeListener = l; + } + + public interface OnKnobChangeListener { + void onValueChanged(RadialKnob knob, int value, boolean fromUser); + + boolean onSwitchChanged(RadialKnob knob, boolean on); + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/preset/InfinitePagerAdapter.java b/src/org/cyanogenmod/audiofx/audiofx/preset/InfinitePagerAdapter.java new file mode 100644 index 0000000..71811f4 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/preset/InfinitePagerAdapter.java @@ -0,0 +1,95 @@ +package com.cyngn.audiofx.preset; + +import android.os.Parcelable; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * A PagerAdapter that wraps around another PagerAdapter to handle paging wrap-around. + */ +public class InfinitePagerAdapter extends PagerAdapter { + + private static final String TAG = "InfinitePagerAdapter"; + private static final boolean DEBUG = false; + + private PagerAdapter adapter; + + public InfinitePagerAdapter(PagerAdapter adapter) { + this.adapter = adapter; + } + + @Override + public int getCount() { + // warning: scrolling to very high values (1,000,000+) results in + // strange drawing behaviour + return Integer.MAX_VALUE; + } + + /** + * @return the {@link #getCount()} result of the wrapped adapter + */ + public int getRealCount() { + return adapter.getCount(); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + int virtualPosition = position % getRealCount(); + debug("instantiateItem: real position: " + position); + debug("instantiateItem: virtual position: " + virtualPosition); + + // only expose virtual position to the inner adapter + return adapter.instantiateItem(container, virtualPosition); + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + int virtualPosition = position % getRealCount(); + debug("destroyItem: real position: " + position); + debug("destroyItem: virtual position: " + virtualPosition); + + // only expose virtual position to the inner adapter + adapter.destroyItem(container, virtualPosition, object); + } + + /* + * Delegate rest of methods directly to the inner adapter. + */ + + @Override + public void finishUpdate(ViewGroup container) { + adapter.finishUpdate(container); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return adapter.isViewFromObject(view, object); + } + + @Override + public void restoreState(Parcelable bundle, ClassLoader classLoader) { + adapter.restoreState(bundle, classLoader); + } + + @Override + public Parcelable saveState() { + return adapter.saveState(); + } + + @Override + public void startUpdate(ViewGroup container) { + adapter.startUpdate(container); + } + + /* + * End delegation + */ + + private void debug(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } +}
\ No newline at end of file diff --git a/src/org/cyanogenmod/audiofx/audiofx/preset/InfiniteViewPager.java b/src/org/cyanogenmod/audiofx/audiofx/preset/InfiniteViewPager.java new file mode 100644 index 0000000..8a4ed38 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/preset/InfiniteViewPager.java @@ -0,0 +1,106 @@ +package com.cyngn.audiofx.preset; + + +import android.content.Context; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.EqualizerManager; +import com.cyngn.audiofx.activity.MasterConfigControl; + +/** + * A {@link ViewPager} that allows pseudo-infinite paging with a wrap-around effect. Should be used with an {@link + * InfinitePagerAdapter}. + */ +public class InfiniteViewPager extends ViewPager { + + private final EqualizerManager mEqManager; + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public InfiniteViewPager(Context context) { + super(context); + mEqManager = MasterConfigControl.getInstance(context).getEqualizerManager(); + } + + public InfiniteViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + mEqManager = MasterConfigControl.getInstance(context).getEqualizerManager(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mEqManager.isAnimatingToCustom()) { + return false; + } + return super.onInterceptTouchEvent(ev); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int textSize = getResources().getDimensionPixelSize(R.dimen.preset_text_size) + + getResources().getDimensionPixelSize(R.dimen.preset_text_padding); + super.onMeasure(widthMeasureSpec, + MeasureSpec.makeMeasureSpec(textSize, MeasureSpec.EXACTLY)); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mEqManager.isAnimatingToCustom()) { + return false; + } + boolean result; + try { + result = super.onTouchEvent(ev); + } catch (IllegalArgumentException e) { + /* There's a bug with the support library where it doesn't check + * the proper pointer index, so when multi touching the container, + * this can sometimes be thrown. Supposedly there are no downsides to just + * catching the exception and moving along, so let's do that.... + */ + result = false; + } + return result; + } + + @Override + public void setAdapter(PagerAdapter adapter) { + super.setAdapter(adapter); + // offset first element so that we can scroll to the left + setCurrentItem(0); + } + + @Override + public void setCurrentItem(int item) { + // offset the current item to ensure there is space to scroll + item = getOffsetAmount() + (item % getAdapter().getCount()); + super.setCurrentItem(item); + } + + public void setCurrentItemAbsolute(int item) { + super.setCurrentItem(item); + } + + private int getOffsetAmount() { + if (getAdapter() instanceof InfinitePagerAdapter) { + InfinitePagerAdapter infAdapter = (InfinitePagerAdapter) getAdapter(); + // allow for 100 back cycles from the beginning + // should be enough to create an illusion of infinity + // warning: scrolling to very high values (1,000,000+) results in + // strange drawing behaviour + return infAdapter.getRealCount() * 100; + } else { + return 0; + } + } + + public void setCurrentItemAbsolute(int newPage, boolean b) { + super.setCurrentItem(newPage, b); + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/preset/PresetPagerAdapter.java b/src/org/cyanogenmod/audiofx/audiofx/preset/PresetPagerAdapter.java new file mode 100644 index 0000000..f6875f7 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/preset/PresetPagerAdapter.java @@ -0,0 +1,82 @@ +/* + * 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. + */ +package com.cyngn.audiofx.preset; + +import android.content.Context; +import android.support.v4.view.PagerAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.cyngn.audiofx.Preset; +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.EqualizerManager; +import com.cyngn.audiofx.activity.MasterConfigControl; + +public class PresetPagerAdapter extends PagerAdapter { + + private final Context mContext; + private final EqualizerManager mEqManager; + + public PresetPagerAdapter(Context context) { + super(); + mContext = context; + mEqManager = MasterConfigControl.getInstance(mContext).getEqualizerManager(); + } + + @Override + public int getItemPosition(Object object) { + View v = (View) object; + int index = mEqManager.indexOf(((Preset) v.getTag())); + if (index == -1) { + return POSITION_NONE; + } else { + return index; + } + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + View view = LayoutInflater.from(mContext) + .inflate(R.layout.preset_adapter_row, container, false); + TextView tv = (TextView) view; + tv.setText(mEqManager.getLocalizedPresetName(position)); + tv.setTag(mEqManager.getPreset(position)); + container.addView(tv); + return view; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (object instanceof View) { + container.removeView((View) object); + } + } + + @Override + public int getCount() { + return mEqManager.getPresetCount(); + } + + @Override + public boolean isViewFromObject(View view, Object o) { + return view == o; + } + + + +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/receiver/QuickSettingsTileReceiver.java b/src/org/cyanogenmod/audiofx/audiofx/receiver/QuickSettingsTileReceiver.java new file mode 100644 index 0000000..c27d44a --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/receiver/QuickSettingsTileReceiver.java @@ -0,0 +1,36 @@ +package com.cyngn.audiofx.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.service.AudioFxService; + +/** + * Created by roman on 1/13/16. + */ +public class QuickSettingsTileReceiver extends BroadcastReceiver { + + private static final boolean DEBUG = false; + private static final String TAG = "QSTileReceiver"; + + public static final String ACTION_TOGGLE_CURRENT_DEVICE + = "com.cyngn.audiofx.action.TOGGLE_DEVICE"; + + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Log.i(TAG, "onReceive() called with " + "context = [" + context + "], intent = [" + intent + "]"); + } + if (ACTION_TOGGLE_CURRENT_DEVICE.equals(intent.getAction())) { + final MasterConfigControl config = MasterConfigControl.getInstance(context); + + config.setCurrentDeviceEnabled(!config.isCurrentDeviceEnabled()); + + // tell service explicitly to update the qs tile in case UI isn't up to let it know + context.startService(new Intent(AudioFxService.ACTION_UPDATE_TILE) + .setClass(context, AudioFxService.class)); + } + } +} diff --git a/src/org/cyanogenmod/audiofx/ServiceDispatcher.java b/src/org/cyanogenmod/audiofx/audiofx/receiver/ServiceDispatcher.java index 7a838b1..7581ba2 100644 --- a/src/org/cyanogenmod/audiofx/ServiceDispatcher.java +++ b/src/org/cyanogenmod/audiofx/audiofx/receiver/ServiceDispatcher.java @@ -1,19 +1,5 @@ -/* - * Copyright (C) 2016 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. - */ -package org.cyanogenmod.audiofx; + +package com.cyngn.audiofx.receiver; import android.content.BroadcastReceiver; import android.content.Context; @@ -21,6 +7,8 @@ import android.content.Intent; import android.media.audiofx.AudioEffect; import android.util.Log; +import com.cyngn.audiofx.service.AudioFxService; + import cyanogenmod.media.AudioSessionInfo; import cyanogenmod.media.CMAudioManager; @@ -28,7 +16,7 @@ public class ServiceDispatcher extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - Intent service = new Intent(context.getApplicationContext(), HeadsetService.class); + Intent service = new Intent(context.getApplicationContext(), AudioFxService.class); String action = intent.getAction(); // We can also get AUDIO_BECOMING_NOISY, which means a device change is @@ -39,10 +27,11 @@ public class ServiceDispatcher extends BroadcastReceiver { String pkg = intent.getStringExtra(AudioEffect.EXTRA_PACKAGE_NAME); service.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sessionId); service.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, pkg); + } else if (action.equals(CMAudioManager.ACTION_AUDIO_SESSIONS_CHANGED)) { // callback from CMAudioService - final AudioSessionInfo info = (AudioSessionInfo)intent.getParcelableExtra( + final AudioSessionInfo info = (AudioSessionInfo) intent.getParcelableExtra( CMAudioManager.EXTRA_SESSION_INFO); boolean added = intent.getBooleanExtra(CMAudioManager.EXTRA_SESSION_ADDED, false); service.putExtra(CMAudioManager.EXTRA_SESSION_INFO, info); @@ -51,5 +40,9 @@ public class ServiceDispatcher extends BroadcastReceiver { service.setAction(action); context.startService(service); + if (AudioFxService.DEBUG) { + Log.d("AudioFX-Dispatcher", "Received " + action); + } + } } diff --git a/src/org/cyanogenmod/audiofx/audiofx/service/AudioFxService.java b/src/org/cyanogenmod/audiofx/audiofx/service/AudioFxService.java new file mode 100644 index 0000000..ce158b5 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/service/AudioFxService.java @@ -0,0 +1,347 @@ +/* + * 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. + * + * Copyright (C) 2016 Cyanogen Inc. + * + * Proprietary and confidential. + */ +package com.cyngn.audiofx.service; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.content.res.Configuration; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.audiofx.AudioEffect; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.ActivityMusic; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.backends.EffectSet; +import com.cyngn.audiofx.receiver.QuickSettingsTileReceiver; + +import java.lang.ref.WeakReference; +import java.util.Locale; + +import cyanogenmod.app.CMStatusBarManager; +import cyanogenmod.app.CustomTile; +import cyanogenmod.media.AudioSessionInfo; +import cyanogenmod.media.CMAudioManager; + +/** + * This service is responsible for applying all requested effects from the AudioFX UI. + * + * Since the AudioFX UI allows for different configurations based on the current output device, + * the service is also responsible for applying the effects properly based on user configuration, + * and the current device output state. + */ +public class AudioFxService extends Service + implements AudioOutputChangeListener.AudioOutputChangedCallback { + + static final String TAG = AudioFxService.class.getSimpleName(); + + public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + public static final boolean ENABLE_REVERB = false; + + public static final String ACTION_DEVICE_OUTPUT_CHANGED + = "org.cyanogenmod.audiofx.ACTION_DEVICE_OUTPUT_CHANGED"; + + public static final String ACTION_UPDATE_TILE = "com.cyngn.audiofx.action.UPDATE_TILE"; + + public static final String EXTRA_DEVICE = "device"; + + // flags for updateService to minimize DSP traffic + public static final int EQ_CHANGED = 0x1; + public static final int BASS_BOOST_CHANGED = 0x2; + public static final int VIRTUALIZER_CHANGED = 0x4; + public static final int TREBLE_BOOST_CHANGED = 0x8; + public static final int VOLUME_BOOST_CHANGED = 0x10; + public static final int REVERB_CHANGED = 0x20; + public static final int ALL_CHANGED = 0xFF; + + // flags from audio.h, used by session callbacks + static final int AUDIO_OUTPUT_FLAG_FAST = 0x4; + static final int AUDIO_OUTPUT_FLAG_DEEP_BUFFER = 0x8; + static final int AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD = 0x10; + + private static final int TILE_ID = 555; + + private Locale mLastLocale; + + private CustomTile mTile; + private CustomTile.Builder mTileBuilder; + + private AudioOutputChangeListener mOutputListener; + private DevicePreferenceManager mDevicePrefs; + private SessionManager mSessionManager; + private Handler mHandler; + + private AudioDeviceInfo mCurrentDevice; + + public static class LocalBinder extends Binder { + + final WeakReference<AudioFxService> mService; + + public LocalBinder(AudioFxService service) {// added a constructor for Stub here + mService = new WeakReference<AudioFxService>(service); + } + + private boolean checkService() { + if (mService.get() == null) { + Log.e("AudioFx-LocalBinder", "Service was null!"); + } + return mService.get() != null; + } + + public void update(int flags) { + if (checkService()) { + mService.get().update(flags); + } + } + + public void setOverrideLevels(short band, float level) { + if (checkService()) { + mService.get().mSessionManager.setOverrideLevels(band, level); + } + } + + public EffectSet getEffect(Integer id) { + if (checkService()) { + return mService.get().mSessionManager.getEffectForSession(id); + } + return null; + } + } + + @Override + public void onCreate() { + super.onCreate(); + if (DEBUG) Log.i(TAG, "Starting service."); + + HandlerThread handlerThread = new HandlerThread("AudioFx-Backend"); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper()); + + mOutputListener = new AudioOutputChangeListener(getApplicationContext(), mHandler); + mOutputListener.addCallback(this); + + mDevicePrefs = new DevicePreferenceManager(getApplicationContext(), mCurrentDevice); + if (!mDevicePrefs.initDefaults()) { + stopSelf(); + return; + } + + mSessionManager = new SessionManager(getApplicationContext(), mHandler, mDevicePrefs, + mCurrentDevice); + mOutputListener.addCallback(mDevicePrefs, mSessionManager); + + final CMAudioManager cma = CMAudioManager.getInstance(getApplicationContext()); + for (AudioSessionInfo asi : cma.listAudioSessions(AudioManager.STREAM_MUSIC)) { + mSessionManager.addSession(asi); + } + + updateQsTile(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (DEBUG) { + Log.i(TAG, "onStartCommand() called with " + "intent = [" + intent + "], flags = [" + + flags + "], startId = [" + startId + "]"); + } + if (intent != null && intent.getAction() != null) { + if (ACTION_UPDATE_TILE.equals(intent.getAction())) { + update(ALL_CHANGED); + } else { + String action = intent.getAction(); + int sessionId = intent.getIntExtra(AudioEffect.EXTRA_AUDIO_SESSION, 0); + String pkg = intent.getStringExtra(AudioEffect.EXTRA_PACKAGE_NAME); + int stream = mapContentTypeToStream( + intent.getIntExtra(AudioEffect.EXTRA_CONTENT_TYPE, + AudioEffect.CONTENT_TYPE_MUSIC)); + + if (action.equals(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)) { + if (DEBUG) { + Log.i(TAG, String.format("New audio session: %d package: %s contentType=%d", + sessionId, pkg, stream)); + } + AudioSessionInfo info = new AudioSessionInfo(sessionId, stream, -1, -1, -1); + mSessionManager.addSession(info); + + } else if (action.equals(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)) { + + AudioSessionInfo info = new AudioSessionInfo(sessionId, stream, -1, -1, -1); + mSessionManager.removeSession(info); + + } else if (action.equals(CMAudioManager.ACTION_AUDIO_SESSIONS_CHANGED)) { + + final AudioSessionInfo info = (AudioSessionInfo) intent.getParcelableExtra( + CMAudioManager.EXTRA_SESSION_INFO); + if (info != null && info.getSessionId() > 0) { + boolean added = intent.getBooleanExtra(CMAudioManager.EXTRA_SESSION_ADDED, + false); + if (added) { + mSessionManager.addSession(info); + } else { + mSessionManager.removeSession(info); + } + } + + } + } + } + return START_STICKY; + } + + /** + * maps {@link AudioEffect#EXTRA_CONTENT_TYPE} to an AudioManager.STREAM_* item + */ + private static int mapContentTypeToStream(int contentType) { + switch (contentType) { + case AudioEffect.CONTENT_TYPE_VOICE: + return AudioManager.STREAM_VOICE_CALL; + case AudioEffect.CONTENT_TYPE_GAME: + // explicitly don't support game effects right now + return -1; + case AudioEffect.CONTENT_TYPE_MOVIE: + case AudioEffect.CONTENT_TYPE_MUSIC: + default: + return AudioManager.STREAM_MUSIC; + } + } + + @Override + public synchronized void onAudioOutputChanged(boolean firstChange, + AudioDeviceInfo outputDevice) { + if (outputDevice == null) { + return; + } + + mCurrentDevice = outputDevice; + + if (DEBUG) + Log.d(TAG, "Broadcasting device changed event"); + + // Update the UI with the change + Intent intent = new Intent(ACTION_DEVICE_OUTPUT_CHANGED); + intent.putExtra("device", outputDevice.getId()); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); + + updateQsTile(); + } + + private void updateQsTile() { + if (mCurrentDevice == null || mDevicePrefs == null) { + // too early + return; + } + if (mTileBuilder == null) { + mTileBuilder = new CustomTile.Builder(this); + } + + mLastLocale = getResources().getConfiguration().locale; + final PendingIntent pi = PendingIntent.getBroadcast(this, 0, + new Intent(QuickSettingsTileReceiver.ACTION_TOGGLE_CURRENT_DEVICE) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + .setClass(this, QuickSettingsTileReceiver.class), 0); + + final PendingIntent longPress = PendingIntent.getActivity(this, 0, + new Intent(this, ActivityMusic.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0); + + String label = getString(R.string.qs_tile_label, + MasterConfigControl.getDeviceDisplayString(this, mCurrentDevice)); + + mTileBuilder + .hasSensitiveData(false) + .setIcon(mDevicePrefs.isGlobalEnabled() ? R.drawable.ic_qs_visualizer_on + : R.drawable.ic_qs_visualizer_off) + .setLabel(label) + .setContentDescription(R.string.qs_tile_content_description) + .shouldCollapsePanel(false) + .setOnClickIntent(pi) + .setOnLongClickIntent(longPress); + + mTile = mTileBuilder.build(); + + CMStatusBarManager.getInstance(this).publishTile(TILE_ID, mTile); + } + + @Override + public void onDestroy() { + if (DEBUG) Log.i(TAG, "Stopping service."); + + mOutputListener.removeCallback(this, mSessionManager, mDevicePrefs); + mSessionManager.onDestroy(); + + CMStatusBarManager.getInstance(this).removeTile(TILE_ID); + + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + return new LocalBinder(this); + } + + @Override + public void onTrimMemory(int level) { + if (DEBUG) Log.d(TAG, "onTrimMemory: level=" + level); + switch (level) { + case TRIM_MEMORY_BACKGROUND: + case TRIM_MEMORY_MODERATE: + case TRIM_MEMORY_RUNNING_MODERATE: + case TRIM_MEMORY_COMPLETE: + if (DEBUG) Log.d(TAG, "killing service if no effects active."); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!mSessionManager.hasActiveSessions()) { + stopSelf(); + Log.w(TAG, "self destructing, no sessions active and nothing to do."); + } + } + }, 1000); + break; + } + } + + /** + * Queue up a backend update. + */ + private void update(int flags) { + mSessionManager.update(flags); + + if ((flags & ALL_CHANGED) == ALL_CHANGED) { + updateQsTile(); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (!mLastLocale.equals(newConfig.locale)) { + updateQsTile(); + } + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/service/AudioOutputChangeListener.java b/src/org/cyanogenmod/audiofx/audiofx/service/AudioOutputChangeListener.java new file mode 100644 index 0000000..f85d9c1 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/service/AudioOutputChangeListener.java @@ -0,0 +1,131 @@ + +package com.cyngn.audiofx.service; + +import static android.media.AudioDeviceInfo.convertDeviceTypeToInternalDevice; + +import android.content.Context; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Handler; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class AudioOutputChangeListener extends AudioDeviceCallback { + + private static final String TAG = "AudioFx-" + AudioOutputChangeListener.class.getSimpleName(); + + private boolean mInitial = true; + + private final Context mContext; + private final AudioManager mAudioManager; + private final Handler mHandler; + private int mLastDevice = -1; + + private final ArrayList<AudioOutputChangedCallback> mCallbacks = new ArrayList<AudioOutputChangedCallback>(); + + public interface AudioOutputChangedCallback { + public void onAudioOutputChanged(boolean firstChange, AudioDeviceInfo outputDevice); + } + + public AudioOutputChangeListener(Context context, Handler handler) { + mContext = context; + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mHandler = handler; + } + + public void addCallback(AudioOutputChangedCallback... callbacks) { + synchronized (mCallbacks) { + boolean initial = mCallbacks.size() == 0; + mCallbacks.addAll(Arrays.asList(callbacks)); + if (initial) { + mAudioManager.registerAudioDeviceCallback(this, mHandler); + } + } + } + + public void removeCallback(AudioOutputChangedCallback... callbacks) { + synchronized (mCallbacks) { + mCallbacks.removeAll(Arrays.asList(callbacks)); + if (mCallbacks.size() == 0) { + mAudioManager.unregisterAudioDeviceCallback(this); + } + } + } + + private void callback() { + synchronized (mCallbacks) { + final AudioDeviceInfo device = getCurrentDevice(); + + if (device == null) { + Log.w(TAG, "Unable to determine audio device!"); + return; + } + + if (mInitial || device.getId() != mLastDevice) { + Log.d(TAG, "onAudioOutputChanged id: " + device.getId() + + " type: " + device.getType() + + " name: " + device.getProductName() + + " address: " + device.getAddress() + + " [" + device.toString() + "]"); + mLastDevice = device.getId(); + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mCallbacks) { + for (AudioOutputChangedCallback callback : mCallbacks) { + callback.onAudioOutputChanged(mInitial, device); + } + } + } + }); + + if (mInitial) { + mInitial = false; + } + } + } + } + + public void refresh() { + callback(); + } + + @Override + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + callback(); + } + + @Override + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + callback(); + } + + public List<AudioDeviceInfo> getConnectedOutputs() { + final List<AudioDeviceInfo> outputs = new ArrayList<AudioDeviceInfo>(); + final int forMusic = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC); + for (AudioDeviceInfo ai : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { + if ((convertDeviceTypeToInternalDevice(ai.getType()) & forMusic) > 0) { + outputs.add(ai); + } + } + return outputs; + } + + public AudioDeviceInfo getCurrentDevice() { + final List<AudioDeviceInfo> devices = getConnectedOutputs(); + return devices.size() > 0 ? devices.get(0) : null; + } + + public AudioDeviceInfo getDeviceById(int id) { + for (AudioDeviceInfo ai : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { + if (ai.getId() == id) { + return ai; + } + } + return null; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/service/BootReceiver.java b/src/org/cyanogenmod/audiofx/audiofx/service/BootReceiver.java new file mode 100644 index 0000000..df853a5 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/service/BootReceiver.java @@ -0,0 +1,14 @@ +package com.cyngn.audiofx.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import com.cyngn.audiofx.Constants; + +public class BootReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + final Intent service = new Intent(context.getApplicationContext(), AudioFxService.class); + context.startService(service); + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/service/DevicePreferenceManager.java b/src/org/cyanogenmod/audiofx/audiofx/service/DevicePreferenceManager.java new file mode 100644 index 0000000..928209e --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/service/DevicePreferenceManager.java @@ -0,0 +1,294 @@ +package com.cyngn.audiofx.service; + +import static com.cyngn.audiofx.Constants.AUDIOFX_GLOBAL_FILE; +import static com.cyngn.audiofx.Constants.AUDIOFX_GLOBAL_HAS_BASSBOOST; +import static com.cyngn.audiofx.Constants.AUDIOFX_GLOBAL_HAS_DTS; +import static com.cyngn.audiofx.Constants.AUDIOFX_GLOBAL_HAS_MAXXAUDIO; +import static com.cyngn.audiofx.Constants.AUDIOFX_GLOBAL_HAS_VIRTUALIZER; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_BASS_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_BASS_STRENGTH; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_EQ_PRESET; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_GLOBAL_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_MAXXVOLUME_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_TREBLE_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_TREBLE_STRENGTH; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_VIRTUALIZER_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH; +import static com.cyngn.audiofx.Constants.DEVICE_HEADSET; +import static com.cyngn.audiofx.Constants.DEVICE_SPEAKER; +import static com.cyngn.audiofx.Constants.EQUALIZER_BAND_LEVEL_RANGE; +import static com.cyngn.audiofx.Constants.EQUALIZER_CENTER_FREQS; +import static com.cyngn.audiofx.Constants.EQUALIZER_NUMBER_OF_BANDS; +import static com.cyngn.audiofx.Constants.EQUALIZER_NUMBER_OF_PRESETS; +import static com.cyngn.audiofx.Constants.EQUALIZER_PRESET; +import static com.cyngn.audiofx.Constants.EQUALIZER_PRESET_NAMES; +import static com.cyngn.audiofx.Constants.SAVED_DEFAULTS; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.media.AudioDeviceInfo; +import android.text.TextUtils; +import android.util.Log; + +import com.cyngn.audiofx.Constants; +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.backends.EffectSet; +import com.cyngn.audiofx.backends.EffectsFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +class DevicePreferenceManager implements AudioOutputChangeListener.AudioOutputChangedCallback { + + private static final String TAG = AudioFxService.TAG; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Context mContext; + + private AudioDeviceInfo mCurrentDevice; + + public DevicePreferenceManager(Context context, AudioDeviceInfo device) { + mContext = context; + mCurrentDevice = device; + } + + public boolean initDefaults() { + try { + saveAndApplyDefaults(false); + } catch (Exception e) { + SharedPreferences prefs = Constants.getGlobalPrefs(mContext); + prefs.edit().clear().commit(); + Log.e(TAG, "Failed to initialize defaults!", e); + return false; + } + return true; + } + + @Override + public void onAudioOutputChanged(boolean firstChange, AudioDeviceInfo outputDevice) { + mCurrentDevice = outputDevice; + } + + public SharedPreferences getCurrentDevicePrefs() { + return mContext.getSharedPreferences( + MasterConfigControl.getDeviceIdentifierString(mCurrentDevice), 0); + } + + public SharedPreferences prefsFor(final String name) { + return mContext.getSharedPreferences(name, 0); + } + + private boolean hasPrefs(final String name) { + return mContext.getSharedPrefsFile(name).exists(); + } + + public boolean isGlobalEnabled() { + return getCurrentDevicePrefs().getBoolean(DEVICE_AUDIOFX_GLOBAL_ENABLE, false); + } + + /** + * This method sets some sane defaults for presets, device defaults, etc + * <p/> + * First we read presets from the system, then adjusts some setting values + * for some better defaults! + */ + private void saveAndApplyDefaults(boolean overridePrevious) { + if (DEBUG) { + Log.d(TAG, "saveAndApplyDefaults() called with overridePrevious = " + + "[" + overridePrevious + "]"); + } + SharedPreferences prefs = Constants.getGlobalPrefs(mContext); + + final int currentPrefVer = prefs.getInt(Constants.AUDIOFX_GLOBAL_PREFS_VERSION_INT, 0); + boolean needsPrefsUpdate = currentPrefVer < Constants.CURRENT_PREFS_INT_VERSION + || overridePrevious; + + if (needsPrefsUpdate) { + Log.d(TAG, "rebuilding presets due to preference upgrade from " + currentPrefVer + + " to " + Constants.CURRENT_PREFS_INT_VERSION); + } + + if (prefs.getBoolean(SAVED_DEFAULTS, false) && !needsPrefsUpdate) { + if (DEBUG) { + Log.e(TAG, "we've already saved defaults and don't need a pref update. aborting."); + } + return; + } + EffectSet temp = new EffectsFactory().createEffectSet(mContext, 0, null); + + final int numBands = temp.getNumEqualizerBands(); + final int numPresets = temp.getNumEqualizerPresets(); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(EQUALIZER_NUMBER_OF_PRESETS, String.valueOf(numPresets)); + editor.putString(EQUALIZER_NUMBER_OF_BANDS, String.valueOf(numBands)); + + // range + short[] rangeShortArr = temp.getEqualizerBandLevelRange(); + editor.putString(EQUALIZER_BAND_LEVEL_RANGE, rangeShortArr[0] + + ";" + rangeShortArr[1]); + + // center freqs + StringBuilder centerFreqs = new StringBuilder(); + // audiofx.global.centerfreqs + for (short i = 0; i < numBands; i++) { + centerFreqs.append(temp.getCenterFrequency(i)); + centerFreqs.append(";"); + + } + centerFreqs.deleteCharAt(centerFreqs.length() - 1); + editor.putString(EQUALIZER_CENTER_FREQS, centerFreqs.toString()); + + // populate preset names + StringBuilder presetNames = new StringBuilder(); + for (int i = 0; i < numPresets; i++) { + String presetName = temp.getEqualizerPresetName((short) i); + presetNames.append(presetName); + presetNames.append("|"); + + // populate preset band values + StringBuilder presetBands = new StringBuilder(); + temp.useEqualizerPreset((short) i); + + for (int j = 0; j < numBands; j++) { + // loop through preset bands + presetBands.append(temp.getEqualizerBandLevel((short) j)); + presetBands.append(";"); + } + presetBands.deleteCharAt(presetBands.length() - 1); + editor.putString(EQUALIZER_PRESET + i, presetBands.toString()); + } + if (presetNames.length() > 0) { + presetNames.deleteCharAt(presetNames.length() - 1); + } + editor.putString(EQUALIZER_PRESET_NAMES, presetNames.toString()); + + editor.putBoolean(AUDIOFX_GLOBAL_HAS_VIRTUALIZER, temp.hasVirtualizer()); + editor.putBoolean(AUDIOFX_GLOBAL_HAS_BASSBOOST, temp.hasBassBoost()); + editor.putBoolean(AUDIOFX_GLOBAL_HAS_MAXXAUDIO, temp.getBrand() == Constants.EFFECT_TYPE_MAXXAUDIO); + editor.putBoolean(AUDIOFX_GLOBAL_HAS_DTS, temp.getBrand() == Constants.EFFECT_TYPE_DTS); + editor.commit(); + temp.release(); + + applyDefaults(needsPrefsUpdate); + + prefs + .edit() + .putInt(Constants.AUDIOFX_GLOBAL_PREFS_VERSION_INT, + Constants.CURRENT_PREFS_INT_VERSION) + .putBoolean(Constants.SAVED_DEFAULTS, true) + .commit(); + } + + private static int findInList(String needle, List<String> haystack) { + for (int i = 0; i < haystack.size(); i++) { + if (haystack.get(i).equalsIgnoreCase(needle)) { + return i; + } + } + return -1; + } + + /** + * This method sets up some *persisted* defaults. + * Prereq: saveDefaults() must have been run before this can apply its defaults properly. + */ + private void applyDefaults(boolean overridePrevious) { + if (DEBUG) { + Log.d(TAG, "applyDefaults() called with overridePrevious = [" + overridePrevious + "]"); + } + + if (!(overridePrevious || !hasPrefs(DEVICE_SPEAKER) || + !hasPrefs(AUDIOFX_GLOBAL_FILE))) { + return; + } + + final SharedPreferences globalPrefs = Constants.getGlobalPrefs(mContext); + + // Nothing to see here for EFFECT_TYPE_DTS + if (globalPrefs.getBoolean(AUDIOFX_GLOBAL_HAS_DTS, false)) { + return; + } + + // set up the builtin speaker configuration + final String smallSpeakers = getNonLocalizedString(R.string.small_speakers); + final List<String> presetNames = new ArrayList<String>(Arrays.asList( + globalPrefs.getString(EQUALIZER_PRESET_NAMES, "").split("\\|"))); + final SharedPreferences speakerPrefs = prefsFor(DEVICE_SPEAKER); + + if (globalPrefs.getBoolean(AUDIOFX_GLOBAL_HAS_MAXXAUDIO, false)) { + // MaxxAudio defaults for builtin speaker: + // maxxvolume: on maxxbass: 40% maxxtreble: 32% + speakerPrefs.edit() + .putBoolean(DEVICE_AUDIOFX_GLOBAL_ENABLE, true) + .putBoolean(DEVICE_AUDIOFX_MAXXVOLUME_ENABLE, true) + .putBoolean(DEVICE_AUDIOFX_BASS_ENABLE, true) + .putString(DEVICE_AUDIOFX_BASS_STRENGTH, "400") + .putBoolean(DEVICE_AUDIOFX_TREBLE_ENABLE, true) + .putString(DEVICE_AUDIOFX_TREBLE_STRENGTH, "32") + .commit(); + + // Defaults for headphones + // maxxvolume: on maxxbass: 20% maxxtreble: 40% maxxspace: 20% + prefsFor(DEVICE_HEADSET).edit() + .putBoolean(DEVICE_AUDIOFX_GLOBAL_ENABLE, true) + .putBoolean(DEVICE_AUDIOFX_MAXXVOLUME_ENABLE, true) + .putBoolean(DEVICE_AUDIOFX_BASS_ENABLE, true) + .putString(DEVICE_AUDIOFX_BASS_STRENGTH, "200") + .putBoolean(DEVICE_AUDIOFX_TREBLE_ENABLE, true) + .putString(DEVICE_AUDIOFX_TREBLE_STRENGTH, "40") + .putBoolean(DEVICE_AUDIOFX_VIRTUALIZER_ENABLE, true) + .putString(DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH, "200") + .commit(); + } else { + // Defaults for headphones + // bass boost: 15% virtualizer: 20% preset: FLAT + int flat = findInList(getNonLocalizedString(R.string.flat), presetNames); + prefsFor(DEVICE_HEADSET).edit() + .putBoolean(DEVICE_AUDIOFX_GLOBAL_ENABLE, true) + .putBoolean(DEVICE_AUDIOFX_BASS_ENABLE, true) + .putString(DEVICE_AUDIOFX_BASS_STRENGTH, "150") + .putBoolean(DEVICE_AUDIOFX_VIRTUALIZER_ENABLE, true) + .putString(DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH, "200") + .putString(DEVICE_AUDIOFX_EQ_PRESET, (flat >= 0 ? String.valueOf(flat) : "0")) + .commit(); + } + + // for 5 band configs, let's add a `Small Speaker` configuration if one + // doesn't exist ( from oss AudioFX: -170;270;50;-220;200 ) + if (Integer.parseInt(globalPrefs.getString(EQUALIZER_NUMBER_OF_BANDS, "0")) == 5 && + findInList(smallSpeakers, presetNames) < 0) { + + int currentPresets = Integer.parseInt( + globalPrefs.getString(EQUALIZER_NUMBER_OF_PRESETS, "0")); + + presetNames.add(smallSpeakers); + String newPresetNames = TextUtils.join("|", presetNames); + globalPrefs.edit() + .putString(EQUALIZER_PRESET + currentPresets, "-170;270;50;-220;200") + .putString(EQUALIZER_PRESET_NAMES, newPresetNames) + .putString(EQUALIZER_NUMBER_OF_PRESETS, Integer.toString(++currentPresets)) + .commit(); + + } + + // set the small speakers preset as the default + int idx = findInList(smallSpeakers, presetNames); + if (idx >= 0) { + speakerPrefs.edit() + .putBoolean(DEVICE_AUDIOFX_GLOBAL_ENABLE, true) + .putString(DEVICE_AUDIOFX_EQ_PRESET, String.valueOf(idx)) + .commit(); + } + } + + private String getNonLocalizedString(int res) { + Configuration config = new Configuration(mContext.getResources().getConfiguration()); + config.setLocale(Locale.ROOT); + return mContext.createConfigurationContext(config).getString(res); + } +} + diff --git a/src/org/cyanogenmod/audiofx/audiofx/service/SessionManager.java b/src/org/cyanogenmod/audiofx/audiofx/service/SessionManager.java new file mode 100644 index 0000000..c744686 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/service/SessionManager.java @@ -0,0 +1,422 @@ +package com.cyngn.audiofx.service; + +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_BASS_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_BASS_STRENGTH; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_EQ_PRESET_LEVELS; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_GLOBAL_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_MAXXVOLUME_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_REVERB_PRESET; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_TREBLE_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_TREBLE_STRENGTH; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_VIRTUALIZER_ENABLE; +import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH; +import static com.cyngn.audiofx.Constants.DEVICE_DEFAULT_GLOBAL_ENABLE; +import static com.cyngn.audiofx.activity.MasterConfigControl.getDeviceIdentifierString; +import static com.cyngn.audiofx.service.AudioFxService.ALL_CHANGED; +import static com.cyngn.audiofx.service.AudioFxService.BASS_BOOST_CHANGED; +import static com.cyngn.audiofx.service.AudioFxService.ENABLE_REVERB; +import static com.cyngn.audiofx.service.AudioFxService.EQ_CHANGED; +import static com.cyngn.audiofx.service.AudioFxService.REVERB_CHANGED; +import static com.cyngn.audiofx.service.AudioFxService.TREBLE_BOOST_CHANGED; +import static com.cyngn.audiofx.service.AudioFxService.VIRTUALIZER_CHANGED; +import static com.cyngn.audiofx.service.AudioFxService.VOLUME_BOOST_CHANGED; + +import android.content.Context; +import android.content.SharedPreferences; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.audiofx.PresetReverb; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.util.SparseArray; + +import com.cyngn.audiofx.backends.EffectSet; +import com.cyngn.audiofx.backends.EffectsFactory; +import com.cyngn.audiofx.eq.EqUtils; + +import cyanogenmod.media.AudioSessionInfo; +import cyanogenmod.media.CMAudioManager; + +class SessionManager implements AudioOutputChangeListener.AudioOutputChangedCallback { + + private static final String TAG = AudioFxService.TAG; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Context mContext; + private final Handler mHandler; + private final DevicePreferenceManager mDevicePrefs; + private final CMAudioManager mCMAudio; + + /** + * All fields ending with L should be locked on {@link #mAudioSessionsL} + */ + private final SparseArray<EffectSet> mAudioSessionsL = new SparseArray<EffectSet>(); + + + private AudioDeviceInfo mCurrentDevice = null; + + // audio priority handler messages + private static final int MSG_UPDATE_DSP = 100; + private static final int MSG_ADD_SESSION = 101; + private static final int MSG_REMOVE_SESSION = 102; + private static final int MSG_UPDATE_FOR_SESSION = 103; + private static final int MSG_UPDATE_EQ_OVERRIDE = 104; + + public SessionManager(Context context, Handler handler, DevicePreferenceManager devicePrefs, + AudioDeviceInfo outputDevice) { + mContext = context; + mCMAudio = CMAudioManager.getInstance(context); + mDevicePrefs = devicePrefs; + mCurrentDevice = outputDevice; + mHandler = new Handler(handler.getLooper(), new AudioServiceHandler()); + } + + public void onDestroy() { + synchronized (mAudioSessionsL) { + mHandler.removeCallbacksAndMessages(null); + mHandler.getLooper().quit(); + } + } + + public void update(int flags) { + if (mHandler == null) { + return; + } + synchronized (mAudioSessionsL) { + mHandler.obtainMessage(MSG_UPDATE_DSP, flags, 0).sendToTarget(); + } + } + + public void setOverrideLevels(short band, float level) { + synchronized (mAudioSessionsL) { + mHandler.obtainMessage(MSG_UPDATE_EQ_OVERRIDE, band, 0, level).sendToTarget(); + } + } + + /** + * Callback which listens for session updates from AudioPolicyManager. This is a + * feature added by CM which notifies when sessions are created or + * destroyed on a particular stream. This is independent of the standard control + * intents and should not conflict with them. This feature may not be available on + * all devices. + * + * Default logic is to do our best to only attach to music streams. We never attach + * to low-latency streams automatically, and we don't attach to mono streams by default + * either since these are usually notifications/ringtones/etc. + */ + public boolean shouldHandleSession(AudioSessionInfo info) { + final boolean music = info.getStream() == AudioManager.STREAM_MUSIC; + final boolean offloaded = (info.getFlags() < 0) + || (info.getFlags() & AudioFxService.AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD) > 0 + || (info.getFlags() & AudioFxService.AUDIO_OUTPUT_FLAG_DEEP_BUFFER) > 0; + final boolean stereo = info.getChannelMask() < 0 || info.getChannelMask() > 1; + + return music && offloaded && stereo && info.getSessionId() > 0; + } + + public void addSession(AudioSessionInfo info) { + synchronized (mAudioSessionsL) { + // Never auto-attach is someone is recording! We don't want to interfere + // with any sort of loopback mechanisms. + final boolean recording = AudioSystem.isSourceActive(0) || AudioSystem.isSourceActive(6); + if (recording) { + Log.w(TAG, "Recording in progress, not performing auto-attach!"); + return; + } + if (shouldHandleSession(info) && + !mHandler.hasMessages(MSG_ADD_SESSION, info.getSessionId())) { + mHandler.removeMessages(MSG_REMOVE_SESSION, info.getSessionId()); + mHandler.obtainMessage(MSG_ADD_SESSION, info.getSessionId()).sendToTarget(); + if (DEBUG) Log.i(TAG, "New audio session: " + info.toString()); + } + } + } + + public void removeSession(AudioSessionInfo info) { + synchronized (mAudioSessionsL) { + if (shouldHandleSession(info) && + !mHandler.hasMessages(MSG_REMOVE_SESSION, info.getSessionId())) { + int sid = info.getSessionId(); + final EffectSet effects = mAudioSessionsL.get(sid); + if (effects != null) { + effects.setMarkedForDeath(true); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_REMOVE_SESSION, sid), + effects.getReleaseDelay()); + if (DEBUG) Log.i(TAG, "Audio session queued for removal: " + info.toString()); + } + } + } + } + + public String getCurrentDeviceIdentifier() { + return getDeviceIdentifierString(mCurrentDevice); + } + + public boolean hasActiveSessions() { + synchronized (mAudioSessionsL) { + return mAudioSessionsL.size() > 0; + } + } + + EffectSet getEffectForSession(int sessionId) { + synchronized (mAudioSessionsL) { + return mAudioSessionsL.get(sessionId); + } + } + + /** + * Update the backend with our changed preferences. + * + * This must only be called from the HandlerThread! + */ + private void updateBackendLocked(int flags, EffectSet session) { + if (Looper.getMainLooper().equals(Looper.myLooper())) { + throw new IllegalStateException("updateBackend must not be called on the UI thread!"); + } + + final SharedPreferences prefs = mDevicePrefs.getCurrentDevicePrefs(); + + if (DEBUG) { + Log.i(TAG, "+++ updateBackend() called with flags=[" + flags + "], session=[" + session + "]"); + } + + if (session == null) { + return; + } + + final boolean globalEnabled = prefs.getBoolean(DEVICE_AUDIOFX_GLOBAL_ENABLE, + DEVICE_DEFAULT_GLOBAL_ENABLE); + + if ((flags & ALL_CHANGED) > 0) { + // global bypass toggle + session.setGlobalEnabled(globalEnabled); + } + + if (globalEnabled) { + // tell the backend it's time to party + if (!session.beginUpdate()) { + Log.e(TAG, "session " + session + " failed to beginUpdate()"); + return; + } + + // equalizer + try { + if ((flags & EQ_CHANGED) > 0) { + // equalizer is always on unless bypassed + session.enableEqualizer(true); + String savedPreset = prefs.getString(DEVICE_AUDIOFX_EQ_PRESET_LEVELS, null); + if (savedPreset != null) { + session.setEqualizerLevelsDecibels(EqUtils.stringBandsToFloats(savedPreset)); + } + } + } catch (Exception e) { + Log.e(TAG, "Error enabling equalizer!", e); + } + + // bass + try { + if ((flags & BASS_BOOST_CHANGED) > 0 && session.hasBassBoost()) { + boolean enable = prefs.getBoolean(DEVICE_AUDIOFX_BASS_ENABLE, false); + session.enableBassBoost(enable); + session.setBassBoostStrength(Short.valueOf(prefs + .getString(DEVICE_AUDIOFX_BASS_STRENGTH, "0"))); + } + } catch (Exception e) { + Log.e(TAG, "Error enabling bass boost!", e); + } + + // reverb + if (ENABLE_REVERB) { + try { + if ((flags & REVERB_CHANGED) > 0 && session.hasReverb()) { + short preset = Short.decode(prefs.getString(DEVICE_AUDIOFX_REVERB_PRESET, + String.valueOf(PresetReverb.PRESET_NONE))); + session.enableReverb(preset > 0); + session.setReverbPreset(preset); + } + } catch (Exception e) { + Log.e(TAG, "Error enabling reverb preset", e); + } + } + + // virtualizer + try { + if ((flags & VIRTUALIZER_CHANGED) > 0 && session.hasVirtualizer()) { + boolean enable = prefs.getBoolean(DEVICE_AUDIOFX_VIRTUALIZER_ENABLE, false); + session.enableVirtualizer(enable); + session.setVirtualizerStrength(Short.valueOf(prefs.getString( + DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH, "0"))); + } + } catch (Exception e) { + Log.e(TAG, "Error enabling virtualizer!"); + } + + // extended audio effects + try { + if ((flags & TREBLE_BOOST_CHANGED) > 0 && session.hasTrebleBoost()) { + // treble + boolean enable = prefs.getBoolean(DEVICE_AUDIOFX_TREBLE_ENABLE, false); + session.enableTrebleBoost(enable); + session.setTrebleBoostStrength(Short.valueOf( + prefs.getString(DEVICE_AUDIOFX_TREBLE_STRENGTH, "0"))); + } + } catch (Exception e) { + Log.e(TAG, "Error enabling treble boost!", e); + } + + try { + if ((flags & VOLUME_BOOST_CHANGED) > 0 && session.hasVolumeBoost()) { + // maxx volume + session.enableVolumeBoost(prefs.getBoolean(DEVICE_AUDIOFX_MAXXVOLUME_ENABLE, false)); + } + } catch (Exception e) { + Log.e(TAG, "Error enabling volume boost!", e); + } + + // mic drop + if (!session.commitUpdate()) { + Log.e(TAG, "session " + session + " failed to commitUpdate()"); + } + } + if (DEBUG) { + Log.i(TAG, "--- updateBackend() called with flags=[" + flags + "], session=[" + session + "]"); + } + } + + private class AudioServiceHandler implements Handler.Callback { + + @Override + public boolean handleMessage(Message msg) { + synchronized (mAudioSessionsL) { + EffectSet session = null; + Integer sessionId = 0; + int flags = 0; + + switch (msg.what) { + case MSG_ADD_SESSION: + /** + * msg.obj = sessionId + */ + sessionId = (Integer) msg.obj; + if (sessionId == null || sessionId <= 0) { + break; + } + + session = mAudioSessionsL.get(sessionId); + if (session == null) { + try { + session = new EffectsFactory() + .createEffectSet(mContext, sessionId, mCurrentDevice); + } catch (Exception e) { + Log.e(TAG, "couldn't create effects for session id: " + sessionId, e); + break; + } + mAudioSessionsL.put(sessionId, session); + if (DEBUG) Log.w(TAG, "added new EffectSet for sessionId=" + sessionId); + updateBackendLocked(ALL_CHANGED, session); + } else { + session.setMarkedForDeath(false); + } + break; + + case MSG_REMOVE_SESSION: + /** + * msg.obj = sessionId + */ + sessionId = (Integer) msg.obj; + if (sessionId == null || sessionId <= 0) { + break; + } + + session = mAudioSessionsL.get(sessionId); + if (session != null && session.isMarkedForDeath()) { + mHandler.removeMessages(MSG_UPDATE_FOR_SESSION, sessionId); + session.release(); + mAudioSessionsL.remove(sessionId); + if (DEBUG) Log.w(TAG, "removed and released sessionId=" + sessionId); + } + + break; + + case MSG_UPDATE_DSP: + /** + * msg.arg1 = update what flags + */ + flags = msg.arg1; + + final String mode = getCurrentDeviceIdentifier(); + if (DEBUG) Log.i(TAG, "Updating to configuration: " + mode); + + final int N = mAudioSessionsL.size(); + for (int i = 0; i < N; i++) { + sessionId = mAudioSessionsL.keyAt(i); + mHandler.obtainMessage(MSG_UPDATE_FOR_SESSION, flags, 0, sessionId).sendToTarget(); + } + break; + + case MSG_UPDATE_FOR_SESSION: + /** + * msg.arg1 = update what flags + * msg.arg2 = unused + * msg.obj = session id integer (for consistency) + */ + sessionId = (Integer) msg.obj; + flags = msg.arg1; + + if (sessionId == null || sessionId <= 0) { + break; + } + + String device = getCurrentDeviceIdentifier(); + if (DEBUG) Log.i(TAG, "updating DSP for sessionId=" + sessionId + + ", device=" + device + " flags=" + flags); + + session = mAudioSessionsL.get(sessionId); + if (session != null) { + updateBackendLocked(flags, session); + } + break; + + case MSG_UPDATE_EQ_OVERRIDE: + for (int i = 0; i < mAudioSessionsL.size(); i++) { + sessionId = mAudioSessionsL.keyAt(i); + session = mAudioSessionsL.get(sessionId); + if (session != null) { + session.setEqualizerBandLevel((short) msg.arg1, (float) msg.obj); + } + } + break; + } + return true; + } + } + } + + /** + * Updates the backend and notifies the frontend when the output device has changed + */ + @Override + public void onAudioOutputChanged(boolean firstChange, AudioDeviceInfo outputDevice) { + synchronized (mAudioSessionsL) { + if (mCurrentDevice == null || + (outputDevice != null && mCurrentDevice.getId() != outputDevice.getId())) { + mCurrentDevice = outputDevice; + } + + EffectSet session = null; + + // Update all the sessions for this output which are moving + final int N = mAudioSessionsL.size(); + for (int i = 0; i < N; i++) { + session = mAudioSessionsL.valueAt(i); + + session.setDevice(mCurrentDevice); + updateBackendLocked(ALL_CHANGED, session); + } + } + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/stats/AppState.java b/src/org/cyanogenmod/audiofx/audiofx/stats/AppState.java new file mode 100644 index 0000000..5417bcd --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/stats/AppState.java @@ -0,0 +1,41 @@ +package com.cyngn.audiofx.stats; + +import com.cyanogen.ambient.analytics.Event; +import com.cyngn.audiofx.Preset; +import com.cyngn.audiofx.activity.MasterConfigControl; +import com.cyngn.audiofx.eq.EqUtils; +import com.cyngn.audiofx.knobs.KnobCommander; + +/** + * Created by roman on 9/29/15. + */ +public class AppState { + public static void appendState(MasterConfigControl control, + KnobCommander knobs, Event.Builder builder) { + // what's the current output device? + builder.addField("state_current_device", control.getCurrentDeviceIdentifier()); + + // what preset? if custom, what name/values? + builder.addField("state_preset_name", control.getEqualizerManager().getCurrentPreset().getName()); + + if (control.getEqualizerManager().getCurrentPreset() instanceof Preset.CustomPreset) { + builder.addField("state_custom_preset_values", + EqUtils.floatLevelsToString(control.getEqualizerManager().getCurrentPreset().getLevels())); + } + + // knob states + if (control.hasMaxxAudio()) { + builder.addField("state_maxx_volume", control.getMaxxVolumeEnabled()); + } + + if (knobs.hasBassBoost()) { + builder.addField("state_knob_bass", knobs.getBassStrength()); + } + if (knobs.hasTreble()) { + builder.addField("state_knob_treble", knobs.getTrebleStrength()); + } + if (knobs.hasVirtualizer()) { + builder.addField("state_knob_virtualizer", knobs.getVirtualizerStrength()); + } + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/stats/UserSession.java b/src/org/cyanogenmod/audiofx/audiofx/stats/UserSession.java new file mode 100644 index 0000000..bb8787c --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/stats/UserSession.java @@ -0,0 +1,202 @@ +package com.cyngn.audiofx.stats; + +import android.os.Parcel; +import android.os.Parcelable; +import com.cyanogen.ambient.analytics.Event; +import com.cyngn.audiofx.Preset; +import com.cyngn.audiofx.knobs.KnobCommander; +import org.json.JSONException; +import org.json.JSONObject; + +public class UserSession implements Parcelable { + + private static final String SOURCE_NONE = "none"; + + private static UserSession sSession; + public static final UserSession getInstance() { + return sSession; + } + + private String mSource; + private int mDevicesChanged; + private int mEnabledDisabledToggles; + private int mPresetsSelected; + private int mPresetsCreated; + private int mPresetsRemoved; + private int mPresetsRenamed; + private int mMaxxVolumeToggled; + private int mTrebleKnobAdjusted; + private int mBassKnobAdjusted; + private int mVirtualizerKnobAdjusted; + + public UserSession(String incomingPackageSource) { + if (incomingPackageSource == null) { + mSource = SOURCE_NONE; + } else { + mSource = incomingPackageSource; + } + sSession = this; + } + + public void deviceChanged() { + mDevicesChanged++; + } + + public void deviceEnabledDisabled() { + mEnabledDisabledToggles++; + } + + public void presetSelected() { + mPresetsSelected++; + } + + public void presetRemoved() { + mPresetsRemoved++; + } + + public void presetRenamed() { + mPresetsRenamed++; + } + + public void presetCreated() { + mPresetsCreated++; + } + + public void maxxVolumeToggled() { + mMaxxVolumeToggled++; + } + + public void knobOptionsAdjusted(int knob) { + switch (knob) { + case KnobCommander.KNOB_BASS: + mBassKnobAdjusted++; + break; + case KnobCommander.KNOB_TREBLE: + mTrebleKnobAdjusted++; + break; + case KnobCommander.KNOB_VIRTUALIZER: + mVirtualizerKnobAdjusted++; + break; + } + } + + public void append(Event.Builder builder) { + builder.addField("session_source", mSource); + if (mDevicesChanged > 0) + builder.addField("session_devices_changed_count", mDevicesChanged); + if (mEnabledDisabledToggles > 0) + builder.addField("session_devices_enabled_disabled_count", mEnabledDisabledToggles); + if (mPresetsSelected > 0) + builder.addField("session_presets_changed_count", mPresetsSelected); + if (mPresetsCreated > 0) + builder.addField("session_presets_created_count", mPresetsCreated); + if (mPresetsRemoved > 0) + builder.addField("session_presets_removed_count", mPresetsRemoved); + if (mPresetsRenamed > 0) + builder.addField("session_presets_renamed_count", mPresetsRenamed); + if (mMaxxVolumeToggled > 0) + builder.addField("session_maxx_volume_toggled", mMaxxVolumeToggled); + if (mBassKnobAdjusted > 0) + builder.addField("session_knobs_bass_adjusted_count", mBassKnobAdjusted); + if (mVirtualizerKnobAdjusted > 0) + builder.addField("session_knobs_virtualizer_adjusted_count", mVirtualizerKnobAdjusted); + if (mTrebleKnobAdjusted > 0) + builder.addField("session_knobs_treble_adjusted_count", mTrebleKnobAdjusted); + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(getClass().getName() + "["); + if (mSource != null) { + s.append("mSource=").append(mSource).append(", "); + } + if (mDevicesChanged > 0) { + s.append("mDevicesChanged=").append(mDevicesChanged).append(", "); + } + if (mEnabledDisabledToggles > 0) { + s.append("mEnabledDisabledToggles=").append(mEnabledDisabledToggles).append(", "); + } + if (mPresetsSelected > 0) { + s.append("mPresetsSelected=").append(mPresetsSelected).append(", "); + } + if (mPresetsCreated > 0) { + s.append("mPresetsCreated=").append(mPresetsCreated).append(", "); + } + if (mPresetsRemoved > 0) { + s.append("mPresetsRemoved=").append(mPresetsRemoved).append(", "); + } + if (mPresetsRenamed > 0) { + s.append("mPresetsRenamed=").append(mPresetsRenamed).append(", "); + } + if (mMaxxVolumeToggled > 0) { + s.append("mMaxxVolumeToggled=").append(mMaxxVolumeToggled).append(", "); + } + if (mBassKnobAdjusted > 0) { + s.append("mBassKnobAdjusted=").append(mBassKnobAdjusted).append(", "); + } + if (mVirtualizerKnobAdjusted > 0) { + s.append("mVirtualizerKnobAdjusted=").append(mVirtualizerKnobAdjusted).append(", "); + } + if (mTrebleKnobAdjusted > 0) { + s.append("mTrebleKnobAdjusted=").append(mTrebleKnobAdjusted).append(", "); + } + if (s.charAt(s.length() - 2) == ',') { + s.delete(s.length() - 2, s.length()); + } + s.append("]"); + + return s.toString(); + } + + public static final Creator<UserSession> CREATOR = new Creator<UserSession>() { + @Override + public UserSession createFromParcel(Parcel in) { + return new UserSession(in); + } + + @Override + public UserSession[] newArray(int size) { + return new UserSession[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + protected UserSession(Parcel in) { + mSource = in.readString(); + mDevicesChanged = in.readInt(); + mEnabledDisabledToggles = in.readInt(); + mPresetsSelected = in.readInt(); + mPresetsCreated = in.readInt(); + mPresetsRemoved = in.readInt(); + mPresetsRenamed = in.readInt(); + mBassKnobAdjusted = in.readInt(); + mVirtualizerKnobAdjusted = in.readInt(); + mTrebleKnobAdjusted = in.readInt(); + mMaxxVolumeToggled = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mSource); + dest.writeInt(mDevicesChanged); + dest.writeInt(mEnabledDisabledToggles); + dest.writeInt(mPresetsSelected); + dest.writeInt(mPresetsCreated); + dest.writeInt(mPresetsRemoved); + dest.writeInt(mPresetsRenamed); + dest.writeInt(mBassKnobAdjusted); + dest.writeInt(mVirtualizerKnobAdjusted); + dest.writeInt(mTrebleKnobAdjusted); + dest.writeInt(mMaxxVolumeToggled); + } + + private static class State { + private String mOutputDevice; + private Preset mPreset; + private String mKnobsOpts; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/viewpagerindicator/CirclePageIndicator.java b/src/org/cyanogenmod/audiofx/audiofx/viewpagerindicator/CirclePageIndicator.java new file mode 100644 index 0000000..b4b4a6b --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/viewpagerindicator/CirclePageIndicator.java @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2011 Patrik Akerfeldt + * Copyright (C) 2011 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.audiofx.viewpagerindicator; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewConfigurationCompat; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import com.cyngn.audiofx.R; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.widget.LinearLayout.HORIZONTAL; +import static android.widget.LinearLayout.VERTICAL; + +/** + * Draws circles (one for each view). The current view position is filled and + * others are only stroked. + */ +public class CirclePageIndicator extends View implements PageIndicator { + private static final int INVALID_POINTER = -1; + + private float mRadius; + private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG); + private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG); + private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG); + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mListener; + private int mCurrentPage; + private int mSnapPage; + private float mPageOffset; + private int mScrollState; + private int mOrientation; + private boolean mCentered; + private boolean mSnap; + + private int mTouchSlop; + private float mLastMotionX = -1; + private int mActivePointerId = INVALID_POINTER; + private boolean mIsDragging; + + + public CirclePageIndicator(Context context) { + this(context, null); + } + + public CirclePageIndicator(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.vpiCirclePageIndicatorStyle); + } + + public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (isInEditMode()) return; + + //Load defaults from resources + final Resources res = getResources(); + final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color); + final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color); + final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation); + final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color); + final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width); + final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius); + final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered); + final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap); + + //Retrieve styles attributes + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0); + + mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered); + mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation); + mPaintPageFill.setStyle(Style.FILL); + mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor)); + mPaintStroke.setStyle(Style.STROKE); + mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor)); + mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth)); + mPaintFill.setStyle(Style.FILL); + mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor)); + mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius); + mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap); + + Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background); + if (background != null) { + setBackgroundDrawable(background); + } + + a.recycle(); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + } + + + public void setCentered(boolean centered) { + mCentered = centered; + invalidate(); + } + + public boolean isCentered() { + return mCentered; + } + + public void setPageColor(int pageColor) { + mPaintPageFill.setColor(pageColor); + invalidate(); + } + + public int getPageColor() { + return mPaintPageFill.getColor(); + } + + public void setFillColor(int fillColor) { + mPaintFill.setColor(fillColor); + invalidate(); + } + + public int getFillColor() { + return mPaintFill.getColor(); + } + + public void setOrientation(int orientation) { + switch (orientation) { + case HORIZONTAL: + case VERTICAL: + mOrientation = orientation; + requestLayout(); + break; + + default: + throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL."); + } + } + + public int getOrientation() { + return mOrientation; + } + + public void setStrokeColor(int strokeColor) { + mPaintStroke.setColor(strokeColor); + invalidate(); + } + + public int getStrokeColor() { + return mPaintStroke.getColor(); + } + + public void setStrokeWidth(float strokeWidth) { + mPaintStroke.setStrokeWidth(strokeWidth); + invalidate(); + } + + public float getStrokeWidth() { + return mPaintStroke.getStrokeWidth(); + } + + public void setRadius(float radius) { + mRadius = radius; + invalidate(); + } + + public float getRadius() { + return mRadius; + } + + public void setSnap(boolean snap) { + mSnap = snap; + invalidate(); + } + + public boolean isSnap() { + return mSnap; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mViewPager == null) { + return; + } + final int count = mViewPager.getAdapter().getCount(); + if (count == 0) { + return; + } + + if (mCurrentPage >= count) { + setCurrentItem(count - 1); + return; + } + + int longSize; + int longPaddingBefore; + int longPaddingAfter; + int shortPaddingBefore; + if (mOrientation == HORIZONTAL) { + longSize = getWidth(); + longPaddingBefore = getPaddingLeft(); + longPaddingAfter = getPaddingRight(); + shortPaddingBefore = getPaddingTop(); + } else { + longSize = getHeight(); + longPaddingBefore = getPaddingTop(); + longPaddingAfter = getPaddingBottom(); + shortPaddingBefore = getPaddingLeft(); + } + + final float threeRadius = mRadius * 3; + final float shortOffset = shortPaddingBefore + mRadius; + float longOffset = longPaddingBefore + mRadius; + if (mCentered) { + longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f); + } + + float dX; + float dY; + + float pageFillRadius = mRadius; + if (mPaintStroke.getStrokeWidth() > 0) { + pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f; + } + + //Draw stroked circles + for (int iLoop = 0; iLoop < count; iLoop++) { + float drawLong = longOffset + (iLoop * threeRadius); + if (mOrientation == HORIZONTAL) { + dX = drawLong; + dY = shortOffset; + } else { + dX = shortOffset; + dY = drawLong; + } + // Only paint fill if not completely transparent + if (mPaintPageFill.getAlpha() > 0) { + canvas.drawCircle(dX, dY, (float) (pageFillRadius/1.5f), mPaintPageFill); + } + + // Only paint stroke if a stroke width was non-zero + if (pageFillRadius != mRadius) { + canvas.drawCircle(dX, dY, mRadius, mPaintStroke); + } + } + + //Draw the filled circle according to the current scroll + float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius; + if (!mSnap) { + cx += mPageOffset * threeRadius; + } + if (mOrientation == HORIZONTAL) { + dX = longOffset + cx; + dY = shortOffset; + } else { + dX = shortOffset; + dY = longOffset + cx; + } + canvas.drawCircle(dX, dY, mRadius, mPaintFill); + } + + public boolean onTouchEvent(android.view.MotionEvent ev) { + if (super.onTouchEvent(ev)) { + return true; + } + if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { + return false; + } + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mLastMotionX = ev.getX(); + break; + + case MotionEvent.ACTION_MOVE: { + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final float deltaX = x - mLastMotionX; + + if (!mIsDragging) { + if (Math.abs(deltaX) > mTouchSlop) { + mIsDragging = true; + } + } + + if (mIsDragging) { + mLastMotionX = x; + if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { + mViewPager.fakeDragBy(deltaX); + } + } + + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (!mIsDragging) { + final int count = mViewPager.getAdapter().getCount(); + final int width = getWidth(); + final float halfWidth = width / 2f; + final float sixthWidth = width / 6f; + + if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage - 1); + } + return true; + } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage + 1); + } + return true; + } + } + + mIsDragging = false; + mActivePointerId = INVALID_POINTER; + if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); + break; + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionX = MotionEventCompat.getX(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + + return true; + } + + @Override + public void setViewPager(ViewPager view) { + if (mViewPager == view) { + return; + } + if (mViewPager != null) { + mViewPager.setOnPageChangeListener(null); + } + if (view.getAdapter() == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + mViewPager = view; + mViewPager.setOnPageChangeListener(this); + invalidate(); + } + + @Override + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + @Override + public void setCurrentItem(int item) { + if (mViewPager == null) { + throw new IllegalStateException("ViewPager has not been bound."); + } + mViewPager.setCurrentItem(item); + mCurrentPage = item; + invalidate(); + } + + @Override + public void notifyDataSetChanged() { + invalidate(); + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mListener != null) { + mListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mCurrentPage = position; + mPageOffset = positionOffset; + invalidate(); + + if (mListener != null) { + mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mCurrentPage = position; + mSnapPage = position; + invalidate(); + } + + if (mListener != null) { + mListener.onPageSelected(position); + } + } + + @Override + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mListener = listener; + } + + /* + * (non-Javadoc) + * + * @see android.view.View#onMeasure(int, int) + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mOrientation == HORIZONTAL) { + setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec)); + } else { + setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec)); + } + } + + /** + * Determines the width of this view + * + * @param measureSpec + * A measureSpec packed into an int + * @return The width of the view, honoring constraints from measureSpec + */ + private int measureLong(int measureSpec) { + int result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) { + //We were told how big to be + result = specSize; + } else { + //Calculate the width according the views count + final int count = mViewPager.getAdapter().getCount(); + result = (int)(getPaddingLeft() + getPaddingRight() + + (count * 2 * mRadius) + (count - 1) * mRadius + 1); + //Respect AT_MOST value if that was what is called for by measureSpec + if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(result, specSize); + } + } + return result; + } + + /** + * Determines the height of this view + * + * @param measureSpec + * A measureSpec packed into an int + * @return The height of the view, honoring constraints from measureSpec + */ + private int measureShort(int measureSpec) { + int result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if (specMode == MeasureSpec.EXACTLY) { + //We were told how big to be + result = specSize; + } else { + //Measure the height + result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1); + //Respect AT_MOST value if that was what is called for by measureSpec + if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(result, specSize); + } + } + return result; + } +} diff --git a/src/org/cyanogenmod/audiofx/audiofx/viewpagerindicator/PageIndicator.java b/src/org/cyanogenmod/audiofx/audiofx/viewpagerindicator/PageIndicator.java new file mode 100644 index 0000000..131d53f --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/viewpagerindicator/PageIndicator.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2011 Patrik Akerfeldt + * Copyright (C) 2011 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyngn.audiofx.viewpagerindicator; + +import android.support.v4.view.ViewPager; + +/** + * A PageIndicator is responsible to show an visual indicator on the total views + * number and the current visible view. + */ +public interface PageIndicator extends ViewPager.OnPageChangeListener { + /** + * Bind the indicator to a ViewPager. + * + * @param view + */ + void setViewPager(ViewPager view); + + /** + * Bind the indicator to a ViewPager. + * + * @param view + * @param initialPosition + */ + void setViewPager(ViewPager view, int initialPosition); + + /** + * <p>Set the current page of both the ViewPager and indicator.</p> + * + * <p>This <strong>must</strong> be used if you need to set the page before + * the views are drawn on screen (e.g., default start page).</p> + * + * @param item + */ + void setCurrentItem(int item); + + /** + * Set a page change listener which will receive forwarded events. + * + * @param listener + */ + void setOnPageChangeListener(ViewPager.OnPageChangeListener listener); + + /** + * Notify the indicator that the fragment list has changed. + */ + void notifyDataSetChanged(); +} diff --git a/src/org/cyanogenmod/audiofx/widget/Biquad.java b/src/org/cyanogenmod/audiofx/audiofx/widget/Biquad.java index f90153d..4486dfe 100644 --- a/src/org/cyanogenmod/audiofx/widget/Biquad.java +++ b/src/org/cyanogenmod/audiofx/audiofx/widget/Biquad.java @@ -1,14 +1,14 @@ -package org.cyanogenmod.audiofx.widget; +package com.cyngn.audiofx.widget; /** * Evaluate transfer functions of biquad filters in direct form 1. * * @author alankila */ -class Biquad { +public class Biquad { private Complex mB0, mB1, mB2, mA0, mA1, mA2; - protected void setHighShelf(double centerFrequency, double samplingFrequency, + public void setHighShelf(double centerFrequency, double samplingFrequency, double dbGain, double slope) { double w0 = 2 * Math.PI * centerFrequency / samplingFrequency; double a = Math.pow(10, dbGain/40); @@ -22,7 +22,7 @@ class Biquad { mA2 = new Complex((a+1) - (a-1) *Math.cos(w0) - 2*Math.sqrt(a)*alpha, 0); } - protected Complex evaluateTransfer(Complex z) { + public 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)); diff --git a/src/org/cyanogenmod/audiofx/widget/Complex.java b/src/org/cyanogenmod/audiofx/audiofx/widget/Complex.java index b4691a3..dbbaaf2 100644 --- a/src/org/cyanogenmod/audiofx/widget/Complex.java +++ b/src/org/cyanogenmod/audiofx/audiofx/widget/Complex.java @@ -1,14 +1,14 @@ -package org.cyanogenmod.audiofx.widget; +package com.cyngn.audiofx.widget; /** * Java support for complex numbers. * * @author alankila */ -class Complex { +public class Complex { private final double mReal, mIm; - protected Complex(double real, double im) { + public Complex(double real, double im) { mReal = real; mIm = im; } @@ -18,7 +18,7 @@ class Complex { * * @return length */ - protected double rho() { + public double rho() { return Math.sqrt(mReal * mReal + mIm * mIm); } @@ -27,7 +27,7 @@ class Complex { * * @return angle in radians */ - protected double theta() { + public double theta() { return Math.atan2(mIm, mReal); } @@ -36,7 +36,7 @@ class Complex { * * @return conjugate */ - protected Complex con() { + public Complex con() { return new Complex(mReal, -mIm); } @@ -46,7 +46,7 @@ class Complex { * @param other * @return sum */ - protected Complex add(Complex other) { + public Complex add(Complex other) { return new Complex(mReal + other.mReal, mIm + other.mIm); } @@ -56,7 +56,7 @@ class Complex { * @param other * @return multiplication result */ - protected Complex mul(Complex other) { + public Complex mul(Complex other) { return new Complex(mReal * other.mReal - mIm * other.mIm, mReal * other.mIm + mIm * other.mReal); } @@ -67,7 +67,7 @@ class Complex { * @param a * @return multiplication result */ - protected Complex mul(double a) { + public Complex mul(double a) { return new Complex(mReal * a, mIm * a); } @@ -77,7 +77,7 @@ class Complex { * @param other * @return division result */ - protected Complex div(Complex other) { + public Complex div(Complex other) { double lengthSquared = other.mReal * other.mReal + other.mIm * other.mIm; return mul(other.con()).div(lengthSquared); } @@ -88,7 +88,7 @@ class Complex { * @param a * @return division result */ - protected Complex div(double a) { + public 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/audiofx/widget/EqualizerSurface.java index ff44c43..2b3c637 100644 --- a/src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java +++ b/src/org/cyanogenmod/audiofx/audiofx/widget/EqualizerSurface.java @@ -17,10 +17,9 @@ * - Modified extensively by cyanogen for multi-band support */ -package org.cyanogenmod.audiofx.widget; +package com.cyngn.audiofx.widget; import android.animation.Animator; -import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; @@ -38,10 +37,8 @@ import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; -import org.cyanogenmod.audiofx.R; +import com.cyngn.audiofx.R; import java.util.Arrays; @@ -77,7 +74,7 @@ public class EqualizerSurface extends SurfaceView implements ValueAnimator.Anima setWillNotDraw(false); mWhite = new Paint(); - mWhite.setColor(getResources().getColor(R.color.white)); + mWhite.setColor(getResources().getColor(R.color.color_grey)); mWhite.setStyle(Style.STROKE); mWhite.setTextSize(mTextSize = context.getResources().getDimensionPixelSize(R.dimen.eq_label_text_size)); mWhite.setTypeface(Typeface.DEFAULT_BOLD); @@ -386,13 +383,17 @@ public class EqualizerSurface extends SurfaceView implements ValueAnimator.Anima 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); +// 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); + + Log.i("eqsurface", i + " level: " + mLevels[i] + ", y: " + y); + String frequencyText = String.format(freq < 1000 ? "%.0f" : "%.0fk", freq < 1000 ? freq : freq / 1000); diff --git a/src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java b/src/org/cyanogenmod/audiofx/audiofx/widget/InterceptableLinearLayout.java index c8d838a..ae0e8a0 100644 --- a/src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java +++ b/src/org/cyanogenmod/audiofx/audiofx/widget/InterceptableLinearLayout.java @@ -26,7 +26,7 @@ * 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; +package com.cyngn.audiofx.widget; import android.content.Context; import android.util.AttributeSet; @@ -53,6 +53,11 @@ public class InterceptableLinearLayout extends LinearLayout { return mIntercept; } + @Override + public boolean hasOverlappingRendering() { + return false; + } + public void setInterception(boolean intercept) { mIntercept = intercept; } diff --git a/src/org/cyanogenmod/audiofx/widget/Gallery.java b/src/org/cyanogenmod/audiofx/widget/Gallery.java deleted file mode 100644 index 8bb5f2a..0000000 --- a/src/org/cyanogenmod/audiofx/widget/Gallery.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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/Knob.java b/src/org/cyanogenmod/audiofx/widget/Knob.java deleted file mode 100644 index ec3312d..0000000 --- a/src/org/cyanogenmod/audiofx/widget/Knob.java +++ /dev/null @@ -1,434 +0,0 @@ -/* - * Copyright (c) 2013, The Linux Foundation. All rights reserved. - * Copyright (c) 2014, The CyanogenMod Project. 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.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -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.Log; -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 String TAG = Knob.class.getSimpleName(); - - private static final int STROKE_WIDTH = 35; - private static final float TEXT_SIZE = 0.20f; - private static final float TEXT_PADDING = 0.31f; - private static final float LABEL_PADDING = 0.02f; - private static final float LABEL_SIZE = 0.08f; - private static final float LABEL_WIDTH = 0.45f; - private static final float INDICATOR_RADIUS = 0.38f; - private ValueAnimator mAnimator; - - public interface OnKnobChangeListener { - void onValueChanged(Knob knob, int value, boolean fromUser); - - boolean onSwitchChanged(Knob knob, boolean on); - - void onAnimationFinished(boolean endValue); - } - - private OnKnobChangeListener mOnKnobChangeListener = null; - - private float mOriginalProgress = 0.0f; - 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 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 fg = (ImageView) findViewById(R.id.knob_foreground); - fg.setImageResource(R.drawable.knob); - - 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); - - mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mPaint.setColor(mHighlightColor); - mPaint.setStrokeWidth(65); - mPaint.setStrokeCap(Paint.Cap.BUTT); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setShadowLayer(2, 1, -2, getResources().getColor(R.color.black)); - - 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) { - mOriginalProgress = ((float) value) / mMax; - if (mOriginalProgress > 100) { - mOriginalProgress = 100; - } else if (mOriginalProgress < 0) { - mOriginalProgress = 0; - } - - setProgress(mOriginalProgress); - } - } - - public void setProgress(float progress) { - setProgress(progress, false); - } - - public void updateProgressText(boolean showText, float progress) { - if (showText) { - mProgressTV.setText((int) (progress * 100) + "%"); - } else { - mProgressTV.setText("--%"); - } - } - - public void setProgress(float progress, boolean fromUser) { - if (progress > 1.0f) { - progress = 1.0f; - } else if (progress < 0.0f) { - progress = 0.0f; - } - - mProgress = progress; - - updateProgressText(mEnabled && mOn, progress); - - 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 = mEnabled ? mKnobOn : mKnobOff; - mKnobOn.setTranslationX((float) Math.sin(mProgress * 2 * Math.PI) * r - mIndicatorWidth / 2); - mKnobOn.setTranslationY((float) -Math.cos(mProgress * 2 * Math.PI) * r - mIndicatorWidth / 2); - } - - @Override - public void setEnabled(boolean enabled) { - mEnabled = enabled; - - mLabelTV.setTextColor(mEnabled ? mHighlightColor : mDisabledColor); - mProgressTV.setTextColor(mEnabled ? mHighlightColor : mDisabledColor); - mPaint.setColor(mEnabled ? mHighlightColor : mDisabledColor); - -// if (enabled) { -// mOn = true; -// } - if (enabled) { - setOn(mOn, false); - } -// updateProgressText(mEnabled && mOn, mOriginalProgress); -// } else { -// } -// invalidate(); - } - - public void setOn(final boolean on, boolean animate) { - mOn = on; - - if (mAnimator != null) { - mAnimator.cancel(); - } - if (mOriginalProgress > 1) { - mOriginalProgress = 1; - } - if (animate) { - if (on) { - mAnimator = ValueAnimator.ofFloat(mProgress, mOriginalProgress); - } else { - mAnimator = ValueAnimator.ofFloat(mProgress, 0f); - } - mAnimator.setDuration(500); - mAnimator.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - mAnimator = null; - updateProgressText(mOn && mEnabled, mOriginalProgress); - if (mOnKnobChangeListener != null) { - mOnKnobChangeListener.onAnimationFinished(on); - } - } - - @Override - public void onAnimationCancel(Animator animation) { - - } - - @Override - public void onAnimationRepeat(Animator animation) { - - } - }); - mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - float progress = (Float) animation.getAnimatedValue(); - if (progress < 0) { - progress = 0; - } else if (progress > 1) { - progress = 1; - } - mProgress = progress; - if (mOnKnobChangeListener != null) { - mOnKnobChangeListener.onValueChanged(Knob.this, (int) (progress * mMax), true); - } - updateProgressText(true, mProgress); - invalidate(); - } - }); - mAnimator.start(); - } else { - updateProgressText(mEnabled && mOn, mOriginalProgress); - - // make progress correct value - mProgress = mOn ? mOriginalProgress : 0f; - - invalidate(); - - if (mOnKnobChangeListener != null) { - mOnKnobChangeListener.onAnimationFinished(on); - } - } - - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - drawIndicator(); - if (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); - mOriginalProgress = mProgress + delta / 360; - if (mOriginalProgress < 0) { - mOriginalProgress = 0; - } else if (mOriginalProgress > 100) { - mOriginalProgress = 100; - } - setProgress(mOriginalProgress, 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, true); - 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; - } -} |