1 /*
2  * Copyright (C) 2010-2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.musicfx;
18 
19 import android.app.ActionBar;
20 import android.app.Activity;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.media.AudioDeviceCallback;
24 import android.media.AudioDeviceInfo;
25 import android.media.AudioFormat;
26 import android.media.AudioManager;
27 import android.media.audiofx.AudioEffect;
28 import android.media.audiofx.AudioEffect.Descriptor;
29 import android.media.audiofx.Virtualizer;
30 import android.os.Bundle;
31 import android.util.Log;
32 import android.view.Gravity;
33 import android.view.MotionEvent;
34 import android.view.View;
35 import android.view.View.OnTouchListener;
36 import android.view.ViewGroup;
37 import android.widget.AdapterView;
38 import android.widget.AdapterView.OnItemSelectedListener;
39 import android.widget.ArrayAdapter;
40 import android.widget.CompoundButton;
41 import android.widget.CompoundButton.OnCheckedChangeListener;
42 import android.widget.SeekBar;
43 import android.widget.SeekBar.OnSeekBarChangeListener;
44 import android.widget.Spinner;
45 import android.widget.Switch;
46 import android.widget.TextView;
47 import android.widget.Toast;
48 
49 import androidx.core.graphics.Insets;
50 import androidx.core.view.ViewCompat;
51 import androidx.core.view.WindowCompat;
52 import androidx.core.view.WindowInsetsCompat;
53 
54 import com.android.audiofx.OpenSLESConstants;
55 
56 import java.util.Formatter;
57 import java.util.HashSet;
58 import java.util.Locale;
59 import java.util.Map;
60 import java.util.Set;
61 
62 /**
63  *
64  */
65 public class ActivityMusic extends Activity implements OnSeekBarChangeListener {
66     private final static String TAG = "MusicFXActivityMusic";
67 
68     /**
69      * Max number of EQ bands supported
70      */
71     private final static int EQUALIZER_MAX_BANDS = 32;
72 
73     /**
74      * Max levels per EQ band in millibels (1 dB = 100 mB)
75      */
76     private final static int EQUALIZER_MAX_LEVEL = 1000;
77 
78     /**
79      * Min levels per EQ band in millibels (1 dB = 100 mB)
80      */
81     private final static int EQUALIZER_MIN_LEVEL = -1000;
82 
83     /**
84      * Indicates if Virtualizer effect is supported.
85      */
86     private boolean mVirtualizerSupported = false;
87     private boolean mVirtualizerIsHeadphoneOnly = false;
88     /**
89      * Indicates if BassBoost effect is supported.
90      */
91     private boolean mBassBoostSupported = false;
92     /**
93      * Indicates if Equalizer effect is supported.
94      */
95     private boolean mEqualizerSupported = false;
96     /**
97      * Indicates if Preset Reverb effect is supported.
98      */
99     private boolean mPresetReverbSupported = false;
100 
101     // Equalizer fields
102     private final SeekBar[] mEqualizerSeekBar = new SeekBar[EQUALIZER_MAX_BANDS];
103     private int mNumberEqualizerBands;
104     private int mEqualizerMinBandLevel;
105     private int mEQPresetUserPos = 1;
106     private int mEQPreset;
107     private int mEQPresetPrevious;
108     private int[] mEQPresetUserBandLevelsPrev;
109     private String[] mEQPresetNames;
110 
111     private int mPRPreset;
112     private int mPRPresetPrevious;
113 
114     private boolean mIsHeadsetOn = false;
115     private CompoundButton mToggleSwitch;
116 
117     private StringBuilder mFormatBuilder = new StringBuilder();
118     private Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
119 
120     /**
121      * Mapping for the EQ widget ids per band
122      */
123     private static final int[][] EQViewElementIds = {
124             { R.id.EQBand0TextView, R.id.EQBand0SeekBar },
125             { R.id.EQBand1TextView, R.id.EQBand1SeekBar },
126             { R.id.EQBand2TextView, R.id.EQBand2SeekBar },
127             { R.id.EQBand3TextView, R.id.EQBand3SeekBar },
128             { R.id.EQBand4TextView, R.id.EQBand4SeekBar },
129             { R.id.EQBand5TextView, R.id.EQBand5SeekBar },
130             { R.id.EQBand6TextView, R.id.EQBand6SeekBar },
131             { R.id.EQBand7TextView, R.id.EQBand7SeekBar },
132             { R.id.EQBand8TextView, R.id.EQBand8SeekBar },
133             { R.id.EQBand9TextView, R.id.EQBand9SeekBar },
134             { R.id.EQBand10TextView, R.id.EQBand10SeekBar },
135             { R.id.EQBand11TextView, R.id.EQBand11SeekBar },
136             { R.id.EQBand12TextView, R.id.EQBand12SeekBar },
137             { R.id.EQBand13TextView, R.id.EQBand13SeekBar },
138             { R.id.EQBand14TextView, R.id.EQBand14SeekBar },
139             { R.id.EQBand15TextView, R.id.EQBand15SeekBar },
140             { R.id.EQBand16TextView, R.id.EQBand16SeekBar },
141             { R.id.EQBand17TextView, R.id.EQBand17SeekBar },
142             { R.id.EQBand18TextView, R.id.EQBand18SeekBar },
143             { R.id.EQBand19TextView, R.id.EQBand19SeekBar },
144             { R.id.EQBand20TextView, R.id.EQBand20SeekBar },
145             { R.id.EQBand21TextView, R.id.EQBand21SeekBar },
146             { R.id.EQBand22TextView, R.id.EQBand22SeekBar },
147             { R.id.EQBand23TextView, R.id.EQBand23SeekBar },
148             { R.id.EQBand24TextView, R.id.EQBand24SeekBar },
149             { R.id.EQBand25TextView, R.id.EQBand25SeekBar },
150             { R.id.EQBand26TextView, R.id.EQBand26SeekBar },
151             { R.id.EQBand27TextView, R.id.EQBand27SeekBar },
152             { R.id.EQBand28TextView, R.id.EQBand28SeekBar },
153             { R.id.EQBand29TextView, R.id.EQBand29SeekBar },
154             { R.id.EQBand30TextView, R.id.EQBand30SeekBar },
155             { R.id.EQBand31TextView, R.id.EQBand31SeekBar } };
156 
157     // Preset Reverb fields
158     /**
159      * Array containing the PR preset names.
160      */
161     private static final String[] PRESETREVERBPRESETSTRINGS = { "None", "SmallRoom", "MediumRoom",
162             "LargeRoom", "MediumHall", "LargeHall", "Plate" };
163 
164     /**
165      * Default localized equalizer preset names. Keep the same as EffectBundle::gEqualizerPresets.
166      */
167     private static final Map<String, Integer> LOCALIZED_EQUALIZER_PRESET_NAMES = Map.of(
168             "Normal", R.string.normal,
169             "Classical", R.string.classical,
170             "Dance", R.string.dance,
171             "Flat", R.string.flat,
172             "Folk", R.string.folk,
173             "Heavy Metal", R.string.heavy_metal,
174             "Hip Hop", R.string.hip_hop,
175             "Jazz", R.string.jazz,
176             "Pop", R.string.pop,
177             "Rock", R.string.rock);
178 
179     /**
180      * Context field
181      */
182     private Context mContext;
183 
184     /**
185      * Calling package name field
186      */
187     private String mCallingPackageName = "empty";
188 
189     // Listen to AudioDeviceCallback to determine if a headset is connected
190     private final AudioDeviceCallback mMyAudioDeviceCallback =
191             new AudioDeviceCallback() {
192 
193                 /**
194                  * Called by the {@link AudioManager} to indicate that one or more audio devices
195                  * have been connected.
196                  *
197                  * @see AudioDeviceCallback.
198                  */
199                 @Override
200                 public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
201                     // do nothing if mIsHeadsetOn is true and new devices added
202                     if (mIsHeadsetOn) {
203                         return;
204                     }
205                     final boolean isHeadsetOn = isHeadSetDeviceConnected();
206                     if (isHeadsetOn) {
207                         Log.v(TAG, " HeadSet connected");
208                         mIsHeadsetOn = true;
209                         updateUIHeadset();
210                     }
211                 }
212 
213                 /**
214                  * Called by the {@link AudioManager} to indicate that one or more audio devices
215                  * have been disconnected.
216                  *
217                  * @param removedDevices An array of {@link AudioDeviceInfo} objects corresponding
218                  *     to any newly removed audio devices.
219                  */
220                 @Override
221                 public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
222                     // do nothing if mIsHeadsetOn is false and some devices removed
223                     if (!mIsHeadsetOn) {
224                         return;
225                     }
226                     final boolean isHeadsetOn = isHeadSetDeviceConnected();
227                     if (!isHeadsetOn) {
228                         Log.v(TAG, " HeadSet disconnected");
229                         mIsHeadsetOn = false;
230                         updateUIHeadset();
231                     }
232                 }
233             };
234 
235     public static final Set<Integer> HEADSET_DEVICE_TYPES = new HashSet<>();
236     static {
237         HEADSET_DEVICE_TYPES.add(AudioDeviceInfo.TYPE_WIRED_HEADSET);
238         HEADSET_DEVICE_TYPES.add(AudioDeviceInfo.TYPE_WIRED_HEADPHONES);
239         HEADSET_DEVICE_TYPES.add(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
240         HEADSET_DEVICE_TYPES.add(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
241         HEADSET_DEVICE_TYPES.add(AudioDeviceInfo.TYPE_USB_HEADSET);
242         HEADSET_DEVICE_TYPES.add(AudioDeviceInfo.TYPE_HEARING_AID);
243         HEADSET_DEVICE_TYPES.add(AudioDeviceInfo.TYPE_BLE_HEADSET);
244     }
245 
isHeadSetDeviceConnected()246     private boolean isHeadSetDeviceConnected() {
247         final AudioManager audioManager = getSystemService(AudioManager.class);
248         final AudioDeviceInfo[] deviceInfos =
249                 audioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
250         for (AudioDeviceInfo deviceInfo : deviceInfos) {
251             if (deviceInfo == null) {
252                 continue;
253             }
254             final int type = deviceInfo.getType();
255             if (HEADSET_DEVICE_TYPES.contains(type)) {
256                 Log.v(TAG, " at least a HeadSet device type " + type + " connected");
257                 return true;
258             }
259         }
260 
261         Log.v(TAG, " no HeadSet device type connected");
262         return false;
263     }
264 
265     /*
266      * Declares and initializes all objects and widgets in the layouts and the CheckBox and SeekBar
267      * onchange methods on creation.
268      *
269      * (non-Javadoc)
270      *
271      * @see android.app.ActivityGroup#onCreate(android.os.Bundle)
272      */
273     @Override
onCreate(final Bundle savedInstanceState)274     public void onCreate(final Bundle savedInstanceState) {
275         super.onCreate(savedInstanceState);
276 
277         // Init context to be used in listeners
278         mContext = this;
279 
280         // Receive intent
281         // get calling intent
282         final Intent intent = getIntent();
283         final int audioSession = intent.getIntExtra(AudioEffect.EXTRA_AUDIO_SESSION,
284                 ControlPanelEffect.AUDIO_SESSION_ID_UNSPECIFIED);
285         Log.v(TAG, "audio session: " + audioSession);
286 
287         mCallingPackageName = getCallingPackage();
288 
289         // check for errors
290         if (mCallingPackageName == null) {
291             Log.e(TAG, "Package name is null");
292             setResult(RESULT_CANCELED);
293             finish();
294             return;
295         }
296         setResult(RESULT_OK);
297 
298         Log.v(TAG, mCallingPackageName + " (" + audioSession + ")");
299 
300         ControlPanelEffect.initEffectsPreferences(mContext, mCallingPackageName, audioSession);
301 
302         // Make sure the package name and the audio session are saved, since MusicFX might be killed
303         // after receiving the AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION intent.
304         ControlPanelEffect.openSession(mContext, mCallingPackageName, audioSession);
305 
306         // query available effects
307         final Descriptor[] effects = AudioEffect.queryEffects();
308 
309         // Determine available/supported effects
310         Log.v(TAG, "Available effects:");
311         for (final Descriptor effect : effects) {
312             Log.v(TAG, effect.name.toString() + ", type: " + effect.type.toString());
313 
314             if (effect.type.equals(AudioEffect.EFFECT_TYPE_VIRTUALIZER)) {
315                 mVirtualizerSupported = true;
316                 mVirtualizerIsHeadphoneOnly = !isVirtualizerTransauralSupported();
317             } else if (effect.type.equals(AudioEffect.EFFECT_TYPE_BASS_BOOST)) {
318                 mBassBoostSupported = true;
319             } else if (effect.type.equals(AudioEffect.EFFECT_TYPE_EQUALIZER)) {
320                 mEqualizerSupported = true;
321             } else if (effect.type.equals(AudioEffect.EFFECT_TYPE_PRESET_REVERB)) {
322                 mPresetReverbSupported = true;
323             }
324         }
325 
326         setContentView(R.layout.music_main);
327         final ViewGroup viewGroup = (ViewGroup) findViewById(R.id.contentSoundEffects);
328 
329         // Set accessibility label for bass boost and virtualizer strength seekbars.
330         findViewById(R.id.bBStrengthText).setLabelFor(R.id.bBStrengthSeekBar);
331         findViewById(R.id.vIStrengthText).setLabelFor(R.id.vIStrengthSeekBar);
332 
333         // Fill array with presets from AudioEffects call.
334         // allocate a space for 2 extra strings (CI Extreme & User)
335         final int numPresets = ControlPanelEffect.getParameterInt(mContext, mCallingPackageName,
336                 ControlPanelEffect.Key.eq_num_presets);
337         mEQPresetNames = new String[numPresets + 2];
338         for (short i = 0; i < numPresets; i++) {
339             mEQPresetNames[i] = ControlPanelEffect.getParameterString(mContext,
340                     mCallingPackageName, ControlPanelEffect.Key.eq_preset_name, i);
341             Integer localizedNameId = LOCALIZED_EQUALIZER_PRESET_NAMES.get(mEQPresetNames[i]);
342             if (localizedNameId != null) {
343                 mEQPresetNames[i] = getString(localizedNameId);
344             }
345         }
346         mEQPresetNames[numPresets] = getString(R.string.ci_extreme);
347         mEQPresetNames[numPresets + 1] = getString(R.string.user);
348         mEQPresetUserPos = numPresets + 1;
349 
350         // Watch for button clicks and initialization.
351         if ((mVirtualizerSupported) || (mBassBoostSupported) || (mEqualizerSupported)
352                 || (mPresetReverbSupported)) {
353             // Set the listener for the main enhancements toggle button.
354             // Depending on the state enable the supported effects if they were
355             // checked in the setup tab.
356             mToggleSwitch = new Switch(this);
357             mToggleSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() {
358                 @Override
359                 public void onCheckedChanged(final CompoundButton buttonView,
360                         final boolean isChecked) {
361 
362                     // set parameter and state
363                     ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
364                             ControlPanelEffect.Key.global_enabled, isChecked);
365                     // Enable Linear layout (in scroll layout) view with all
366                     // effect contents depending on checked state
367                     setEnabledAllChildren(viewGroup, isChecked);
368                     // update UI according to headset state
369                     updateUIHeadset();
370                 }
371             });
372 
373             // Initialize the Virtualizer elements.
374             // Set the SeekBar listener.
375             if (mVirtualizerSupported) {
376                 // Show msg when disabled slider (layout) is touched
377                 findViewById(R.id.vILayout).setOnTouchListener(new OnTouchListener() {
378 
379                     @Override
380                     public boolean onTouch(final View v, final MotionEvent event) {
381                         if (event.getAction() == MotionEvent.ACTION_UP) {
382                             showHeadsetMsg();
383                         }
384                         return false;
385                     }
386                 });
387 
388                 final SeekBar seekbar = (SeekBar) findViewById(R.id.vIStrengthSeekBar);
389                 seekbar.setMax(OpenSLESConstants.VIRTUALIZER_MAX_STRENGTH
390                         - OpenSLESConstants.VIRTUALIZER_MIN_STRENGTH);
391 
392                 seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
393                     // Update the parameters while SeekBar changes and set the
394                     // effect parameter.
395 
396                     @Override
397                     public void onProgressChanged(final SeekBar seekBar, final int progress,
398                             final boolean fromUser) {
399                         // set parameter and state
400                         ControlPanelEffect.setParameterInt(mContext, mCallingPackageName,
401                                 ControlPanelEffect.Key.virt_strength, progress);
402                     }
403 
404                     // If slider pos was 0 when starting re-enable effect
405                     @Override
406                     public void onStartTrackingTouch(final SeekBar seekBar) {
407                         if (seekBar.getProgress() == 0) {
408                             ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
409                                     ControlPanelEffect.Key.virt_enabled, true);
410                         }
411                     }
412 
413                     // If slider pos = 0 when stopping disable effect
414                     @Override
415                     public void onStopTrackingTouch(final SeekBar seekBar) {
416                         if (seekBar.getProgress() == 0) {
417                             // disable
418                             ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
419                                     ControlPanelEffect.Key.virt_enabled, false);
420                         }
421                     }
422                 });
423 
424                 final Switch sw = (Switch) findViewById(R.id.vIStrengthToggle);
425                 sw.setOnCheckedChangeListener(new OnCheckedChangeListener() {
426                     @Override
427                     public void onCheckedChanged(final CompoundButton buttonView,
428                             final boolean isChecked) {
429                         ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
430                                 ControlPanelEffect.Key.virt_enabled, isChecked);
431                     }
432                 });
433             } else {
434                 findViewById(R.id.vILayout).setVisibility(View.GONE);
435             }
436 
437             // Initialize the Bass Boost elements.
438             // Set the SeekBar listener.
439             if (mBassBoostSupported) {
440                 // Show msg when disabled slider (layout) is touched
441                 findViewById(R.id.bBLayout).setOnTouchListener(new OnTouchListener() {
442 
443                     @Override
444                     public boolean onTouch(final View v, final MotionEvent event) {
445                         if (event.getAction() == MotionEvent.ACTION_UP) {
446                             showHeadsetMsg();
447                         }
448                         return false;
449                     }
450                 });
451 
452                 final SeekBar seekbar = (SeekBar) findViewById(R.id.bBStrengthSeekBar);
453                 seekbar.setMax(OpenSLESConstants.BASSBOOST_MAX_STRENGTH
454                         - OpenSLESConstants.BASSBOOST_MIN_STRENGTH);
455 
456                 seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
457                     // Update the parameters while SeekBar changes and set the
458                     // effect parameter.
459 
460                     @Override
461                     public void onProgressChanged(final SeekBar seekBar, final int progress,
462                             final boolean fromUser) {
463                         // set parameter and state
464                         ControlPanelEffect.setParameterInt(mContext, mCallingPackageName,
465                                 ControlPanelEffect.Key.bb_strength, progress);
466                     }
467 
468                     // If slider pos was 0 when starting re-enable effect
469                     @Override
470                     public void onStartTrackingTouch(final SeekBar seekBar) {
471                         if (seekBar.getProgress() == 0) {
472                             ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
473                                     ControlPanelEffect.Key.bb_enabled, true);
474                         }
475                     }
476 
477                     // If slider pos = 0 when stopping disable effect
478                     @Override
479                     public void onStopTrackingTouch(final SeekBar seekBar) {
480                         if (seekBar.getProgress() == 0) {
481                             // disable
482                             ControlPanelEffect.setParameterBoolean(mContext, mCallingPackageName,
483                                     ControlPanelEffect.Key.bb_enabled, false);
484                         }
485                     }
486                 });
487             } else {
488                 findViewById(R.id.bBLayout).setVisibility(View.GONE);
489             }
490 
491             // Initialize the Equalizer elements.
492             if (mEqualizerSupported) {
493                 mEQPreset = ControlPanelEffect.getParameterInt(mContext, mCallingPackageName,
494                         ControlPanelEffect.Key.eq_current_preset);
495                 if (mEQPreset >= mEQPresetNames.length) {
496                     mEQPreset = 0;
497                 }
498                 mEQPresetPrevious = mEQPreset;
499                 equalizerSpinnerInit((Spinner)findViewById(R.id.eqSpinner));
500                 equalizerBandsInit(findViewById(R.id.eqcontainer));
501             } else {
502                 findViewById(R.id.eqSpinner).setVisibility(View.GONE);
503             }
504 
505             // Initialize the Preset Reverb elements.
506             // Set Spinner listeners.
507             if (mPresetReverbSupported) {
508                 mPRPreset = ControlPanelEffect.getParameterInt(mContext, mCallingPackageName,
509                         ControlPanelEffect.Key.pr_current_preset);
510                 mPRPresetPrevious = mPRPreset;
511                 reverbSpinnerInit((Spinner)findViewById(R.id.prSpinner));
512             } else {
513                 findViewById(R.id.prSpinner).setVisibility(View.GONE);
514             }
515 
516             ActionBar ab = getActionBar();
517             final int padding = getResources().getDimensionPixelSize(
518                     R.dimen.action_bar_switch_padding);
519             mToggleSwitch.setPadding(0,0, padding, 0);
520             ab.setCustomView(mToggleSwitch, new ActionBar.LayoutParams(
521                     ActionBar.LayoutParams.WRAP_CONTENT,
522                     ActionBar.LayoutParams.WRAP_CONTENT,
523                     Gravity.CENTER_VERTICAL | Gravity.RIGHT));
524             ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
525         } else {
526             viewGroup.setVisibility(View.GONE);
527             ((TextView) findViewById(R.id.noEffectsTextView)).setVisibility(View.VISIBLE);
528         }
529 
530         if (com.android.media.audio.Flags.musicFxEdgeToEdge()) {
531             setupEdgeToEdge();
532         }
533     }
534 
535     /*
536      * (non-Javadoc)
537      *
538      * @see android.app.Activity#onResume()
539      */
540     @Override
onResume()541     protected void onResume() {
542         super.onResume();
543         if ((mVirtualizerSupported) || (mBassBoostSupported) || (mEqualizerSupported)
544                 || (mPresetReverbSupported)) {
545             // Monitor AudioDeviceCallback for device change
546             final AudioManager audioManager = getSystemService(AudioManager.class);
547             audioManager.registerAudioDeviceCallback(mMyAudioDeviceCallback, null);
548             mIsHeadsetOn = isHeadSetDeviceConnected();
549             Log.v(TAG, "onResume: mIsHeadsetOn : " + mIsHeadsetOn);
550             // Update UI
551             updateUI();
552         }
553     }
554 
555     /*
556      * (non-Javadoc)
557      *
558      * @see android.app.Activity#onPause()
559      */
560     @Override
onPause()561     protected void onPause() {
562         super.onPause();
563 
564         // Stop monitoring AudioDeviceCallback, (these affect the visible UI, so we only care about
565         // them while we're in the foreground).
566         if ((mVirtualizerSupported)
567                 || (mBassBoostSupported)
568                 || (mEqualizerSupported)
569                 || (mPresetReverbSupported)) {
570             final AudioManager audioManager = getSystemService(AudioManager.class);
571             audioManager.unregisterAudioDeviceCallback(mMyAudioDeviceCallback);
572         }
573     }
574 
reverbSpinnerInit(Spinner spinner)575     private void reverbSpinnerInit(Spinner spinner) {
576         ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
577                 android.R.layout.simple_spinner_item, PRESETREVERBPRESETSTRINGS);
578         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
579         spinner.setAdapter(adapter);
580         spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
581 
582             @Override
583             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
584                 if (position != mPRPresetPrevious) {
585                     presetReverbSetPreset(position);
586                 }
587                 mPRPresetPrevious = position;
588             }
589 
590             @Override
591             public void onNothingSelected(AdapterView<?> parent) {
592             }
593         });
594         spinner.setSelection(mPRPreset);
595     }
596 
equalizerSpinnerInit(Spinner spinner)597     private void equalizerSpinnerInit(Spinner spinner) {
598         ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
599                 android.R.layout.simple_spinner_item, mEQPresetNames);
600         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
601         spinner.setAdapter(adapter);
602         spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
603 
604             @Override
605             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
606                 if (position != mEQPresetPrevious) {
607                     equalizerSetPreset(position);
608                 }
609                 mEQPresetPrevious = position;
610             }
611 
612             @Override
613             public void onNothingSelected(AdapterView<?> parent) {
614             }
615         });
616         spinner.setSelection(mEQPreset);
617     }
618 
619 
620     /**
621      * En/disables all children for a given view. For linear and relative layout children do this
622      * recursively
623      *
624      * @param viewGroup
625      * @param enabled
626      */
setEnabledAllChildren(final ViewGroup viewGroup, final boolean enabled)627     private void setEnabledAllChildren(final ViewGroup viewGroup, final boolean enabled) {
628         final int count = viewGroup.getChildCount();
629         for (int i = 0; i < count; i++) {
630             final View view = viewGroup.getChildAt(i);
631             if ((view instanceof ViewGroup)) {
632                 final ViewGroup vg = (ViewGroup) view;
633                 setEnabledAllChildren(vg, enabled);
634             }
635             view.setEnabled(enabled);
636         }
637     }
638 
639     /**
640      * Updates UI (checkbox, seekbars, enabled states) according to the current stored preferences.
641      */
updateUI()642     private void updateUI() {
643         final boolean isEnabled = ControlPanelEffect.getParameterBoolean(mContext,
644                 mCallingPackageName, ControlPanelEffect.Key.global_enabled);
645         mToggleSwitch.setChecked(isEnabled);
646         setEnabledAllChildren((ViewGroup) findViewById(R.id.contentSoundEffects), isEnabled);
647         updateUIHeadset();
648 
649         if (mVirtualizerSupported) {
650             SeekBar bar = (SeekBar) findViewById(R.id.vIStrengthSeekBar);
651             Switch sw = (Switch) findViewById(R.id.vIStrengthToggle);
652             int strength = ControlPanelEffect
653                     .getParameterInt(mContext, mCallingPackageName,
654                             ControlPanelEffect.Key.virt_strength);
655             bar.setProgress(strength);
656             boolean hasStrength = ControlPanelEffect.getParameterBoolean(mContext,
657                     mCallingPackageName, ControlPanelEffect.Key.virt_strength_supported);
658             if (hasStrength) {
659                 sw.setVisibility(View.GONE);
660             } else {
661                 bar.setVisibility(View.GONE);
662                 sw.setChecked(sw.isEnabled() && strength != 0);
663             }
664         }
665         if (mBassBoostSupported) {
666             ((SeekBar) findViewById(R.id.bBStrengthSeekBar)).setProgress(ControlPanelEffect
667                     .getParameterInt(mContext, mCallingPackageName,
668                             ControlPanelEffect.Key.bb_strength));
669         }
670         if (mEqualizerSupported) {
671             equalizerUpdateDisplay();
672         }
673         if (mPresetReverbSupported) {
674             int reverb = ControlPanelEffect.getParameterInt(
675                                     mContext, mCallingPackageName,
676                                     ControlPanelEffect.Key.pr_current_preset);
677             ((Spinner)findViewById(R.id.prSpinner)).setSelection(reverb);
678         }
679     }
680 
681     /**
682      * Updates UI for headset mode. En/disable VI and BB controls depending on headset state
683      * (on/off) if effects are on. Do the inverse for their layouts so they can take over
684      * control/events.
685      */
updateUIHeadset()686     private void updateUIHeadset() {
687         if (mToggleSwitch.isChecked()) {
688             if (mVirtualizerSupported) {
689                 // virtualizer is on if headset is on or transaural virtualizer supported
690                 final boolean isVirtualizerOn = mIsHeadsetOn || !mVirtualizerIsHeadphoneOnly;
691                 ControlPanelEffect.setParameterBoolean(
692                         mContext,
693                         mCallingPackageName,
694                         ControlPanelEffect.Key.virt_enabled,
695                         isVirtualizerOn);
696                 ((TextView) findViewById(R.id.vIStrengthText)).setEnabled(isVirtualizerOn);
697                 ((SeekBar) findViewById(R.id.vIStrengthSeekBar)).setEnabled(isVirtualizerOn);
698                 findViewById(R.id.vILayout).setEnabled(!isVirtualizerOn);
699             }
700             if (mBassBoostSupported) {
701                 ControlPanelEffect.setParameterBoolean(
702                         mContext,
703                         mCallingPackageName,
704                         ControlPanelEffect.Key.bb_enabled,
705                         mIsHeadsetOn);
706                 ((TextView) findViewById(R.id.bBStrengthText)).setEnabled(mIsHeadsetOn);
707                 ((SeekBar) findViewById(R.id.bBStrengthSeekBar)).setEnabled(mIsHeadsetOn);
708                 findViewById(R.id.bBLayout).setEnabled(!mIsHeadsetOn);
709             }
710         }
711     }
712 
713     /**
714      * Initializes the equalizer elements. Set the SeekBars and Spinner listeners.
715      */
equalizerBandsInit(View eqcontainer)716     private void equalizerBandsInit(View eqcontainer) {
717         // Initialize the N-Band Equalizer elements.
718         mNumberEqualizerBands = ControlPanelEffect.getParameterInt(mContext, mCallingPackageName,
719                 ControlPanelEffect.Key.eq_num_bands);
720         mEQPresetUserBandLevelsPrev = ControlPanelEffect.getParameterIntArray(mContext,
721                 mCallingPackageName, ControlPanelEffect.Key.eq_preset_user_band_level);
722         final int[] centerFreqs = ControlPanelEffect.getParameterIntArray(mContext,
723                 mCallingPackageName, ControlPanelEffect.Key.eq_center_freq);
724         final int[] bandLevelRange = ControlPanelEffect.getParameterIntArray(mContext,
725                 mCallingPackageName, ControlPanelEffect.Key.eq_level_range);
726         mEqualizerMinBandLevel = (int) Math.max(EQUALIZER_MIN_LEVEL, bandLevelRange[0]);
727         final int mEqualizerMaxBandLevel = (int) Math.min(EQUALIZER_MAX_LEVEL, bandLevelRange[1]);
728 
729         for (int band = 0; band < mNumberEqualizerBands; band++) {
730             // Unit conversion from mHz to Hz and use k prefix if necessary to display
731             final int centerFreq = centerFreqs[band] / 1000;
732             float centerFreqHz = centerFreq;
733             String unitPrefix = "";
734             if (centerFreqHz >= 1000) {
735                 centerFreqHz = centerFreqHz / 1000;
736                 unitPrefix = "k";
737             }
738             ((TextView) eqcontainer.findViewById(EQViewElementIds[band][0])).setText(
739                     format("%.0f ", centerFreqHz) + unitPrefix + "Hz");
740             mEqualizerSeekBar[band] = (SeekBar) eqcontainer
741                     .findViewById(EQViewElementIds[band][1]);
742             eqcontainer.findViewById(EQViewElementIds[band][0])
743                     .setLabelFor(EQViewElementIds[band][1]);
744             mEqualizerSeekBar[band].setMax(mEqualizerMaxBandLevel - mEqualizerMinBandLevel);
745             mEqualizerSeekBar[band].setOnSeekBarChangeListener(this);
746         }
747 
748         // Hide the inactive Equalizer bands.
749         for (int band = mNumberEqualizerBands; band < EQUALIZER_MAX_BANDS; band++) {
750             // CenterFreq text
751             eqcontainer.findViewById(EQViewElementIds[band][0]).setVisibility(View.GONE);
752             // SeekBar
753             eqcontainer.findViewById(EQViewElementIds[band][1]).setVisibility(View.GONE);
754         }
755 
756         TextView tv = (TextView) findViewById(R.id.maxLevelText);
757         tv.setText(String.format("+%d dB", (int) Math.ceil(mEqualizerMaxBandLevel / 100)));
758         tv = (TextView) findViewById(R.id.centerLevelText);
759         tv.setText("0 dB");
760         tv = (TextView) findViewById(R.id.minLevelText);
761         tv.setText(String.format("%d dB", (int) Math.floor(mEqualizerMinBandLevel / 100)));
762         equalizerUpdateDisplay();
763     }
764 
format(String format, Object... args)765     private String format(String format, Object... args) {
766         mFormatBuilder.setLength(0);
767         mFormatter.format(format, args);
768         return mFormatBuilder.toString();
769     }
770 
771     /*
772      * For the EQ Band SeekBars
773      *
774      * (non-Javadoc)
775      *
776      * @see android.widget.SeekBar.OnSeekBarChangeListener#onProgressChanged(android
777      * .widget.SeekBar, int, boolean)
778      */
779 
780     @Override
onProgressChanged(final SeekBar seekbar, final int progress, final boolean fromUser)781     public void onProgressChanged(final SeekBar seekbar, final int progress, final boolean fromUser) {
782         final int id = seekbar.getId();
783 
784         for (short band = 0; band < mNumberEqualizerBands; band++) {
785             if (id == EQViewElementIds[band][1]) {
786                 final short level = (short) (progress + mEqualizerMinBandLevel);
787                 if (fromUser) {
788                     equalizerBandUpdate(band, level);
789                 }
790                 break;
791             }
792         }
793     }
794 
795     /*
796      * (non-Javadoc)
797      *
798      * @see android.widget.SeekBar.OnSeekBarChangeListener#onStartTrackingTouch(android
799      * .widget.SeekBar)
800      */
801 
802     @Override
onStartTrackingTouch(final SeekBar seekbar)803     public void onStartTrackingTouch(final SeekBar seekbar) {
804         // get current levels
805         final int[] bandLevels = ControlPanelEffect.getParameterIntArray(mContext,
806                 mCallingPackageName, ControlPanelEffect.Key.eq_band_level);
807         // copy current levels to user preset
808         for (short band = 0; band < mNumberEqualizerBands; band++) {
809             equalizerBandUpdate(band, bandLevels[band]);
810         }
811         equalizerSetPreset(mEQPresetUserPos);
812         ((Spinner)findViewById(R.id.eqSpinner)).setSelection(mEQPresetUserPos);
813     }
814 
815     /*
816      * Updates the EQ display when the user stops changing.
817      *
818      * (non-Javadoc)
819      *
820      * @see android.widget.SeekBar.OnSeekBarChangeListener#onStopTrackingTouch(android
821      * .widget.SeekBar)
822      */
823 
824     @Override
onStopTrackingTouch(final SeekBar seekbar)825     public void onStopTrackingTouch(final SeekBar seekbar) {
826         equalizerUpdateDisplay();
827     }
828 
829     /**
830      * Updates the EQ by getting the parameters.
831      */
equalizerUpdateDisplay()832     private void equalizerUpdateDisplay() {
833         // Update and show the active N-Band Equalizer bands.
834         final int[] bandLevels = ControlPanelEffect.getParameterIntArray(mContext,
835                 mCallingPackageName, ControlPanelEffect.Key.eq_band_level);
836         for (short band = 0; band < mNumberEqualizerBands; band++) {
837             final int level = bandLevels[band];
838             final int progress = level - mEqualizerMinBandLevel;
839             mEqualizerSeekBar[band].setProgress(progress);
840         }
841     }
842 
843     /**
844      * Updates/sets a given EQ band level.
845      *
846      * @param band
847      *            Band id
848      * @param level
849      *            EQ band level
850      */
equalizerBandUpdate(final int band, final int level)851     private void equalizerBandUpdate(final int band, final int level) {
852         ControlPanelEffect.setParameterInt(mContext, mCallingPackageName,
853                 ControlPanelEffect.Key.eq_band_level, level, band);
854     }
855 
856     /**
857      * Sets the given EQ preset.
858      *
859      * @param preset
860      *            EQ preset id.
861      */
equalizerSetPreset(final int preset)862     private void equalizerSetPreset(final int preset) {
863         ControlPanelEffect.setParameterInt(mContext, mCallingPackageName,
864                 ControlPanelEffect.Key.eq_current_preset, preset);
865         equalizerUpdateDisplay();
866     }
867 
868     /**
869      * Sets the given PR preset.
870      *
871      * @param preset
872      *            PR preset id.
873      */
presetReverbSetPreset(final int preset)874     private void presetReverbSetPreset(final int preset) {
875         ControlPanelEffect.setParameterInt(mContext, mCallingPackageName,
876                 ControlPanelEffect.Key.pr_current_preset, preset);
877     }
878 
879     /**
880      * Show msg that headset needs to be plugged.
881      */
showHeadsetMsg()882     private void showHeadsetMsg() {
883         final Context context = getApplicationContext();
884         final int duration = Toast.LENGTH_SHORT;
885 
886         final Toast toast = Toast.makeText(context, getString(R.string.headset_plug), duration);
887         toast.setGravity(Gravity.CENTER, toast.getXOffset() / 2, toast.getYOffset() / 2);
888         toast.show();
889     }
890 
isVirtualizerTransauralSupported()891     private static boolean isVirtualizerTransauralSupported() {
892         Virtualizer virt = null;
893         boolean transauralSupported = false;
894         try {
895             virt = new Virtualizer(0, android.media.AudioSystem.newAudioSessionId());
896             transauralSupported = virt.canVirtualize(AudioFormat.CHANNEL_OUT_STEREO,
897                     Virtualizer.VIRTUALIZATION_MODE_TRANSAURAL);
898         } catch (Exception e) {
899         } finally {
900             if (virt != null) {
901                 virt.release();
902             }
903         }
904         return transauralSupported;
905     }
906 
setupEdgeToEdge()907     private void setupEdgeToEdge() {
908         WindowCompat.setDecorFitsSystemWindows(getWindow(), false /* decorFitsSystemWindows */);
909         ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.contentSoundEffects),
910                 (v, windowInsets) -> {
911                     Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
912                     // Apply the insets paddings to the view.
913                     v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
914 
915                     // Return CONSUMED if you don't want the window insets to keep being
916                     // passed down to descendant views.
917                     return WindowInsetsCompat.CONSUMED;
918                 });
919     }
920 }
921