OSDN Git Service

Eleven: Add equalizer visualization
authorlinus_lee <llee@cyngn.com>
Thu, 20 Nov 2014 01:52:53 +0000 (17:52 -0800)
committerlinus_lee <llee@cyngn.com>
Mon, 8 Dec 2014 23:19:02 +0000 (15:19 -0800)
Change-Id: I9a3112cf4138e916ed53571236e54b67c30b53c4

13 files changed:
Android.mk
AndroidManifest.xml
res/drawable/equalizer_background.9.png [new file with mode: 0644]
res/layout/activity_player_fragment.xml
res/values-xhdpi/equalizer_config.xml [new file with mode: 0644]
res/values-xxhdpi/equalizer_config.xml [new file with mode: 0644]
res/values/equalizer_config.xml [new file with mode: 0644]
res/values/strings.xml
res/xml/settings.xml
src/com/cyngn/eleven/ui/activities/SlidingPanelActivity.java
src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java
src/com/cyngn/eleven/utils/PreferenceUtils.java
src/com/cyngn/eleven/widgets/EqualizerView.java [new file with mode: 0644]

index 3f7501b..8a7828c 100644 (file)
@@ -9,6 +9,7 @@ LOCAL_SRC_FILES += $(call all-java-files-under, src)
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-v8-renderscript \
     android-common \
+    android-visualizer \
     eleven_support_v4 \
     eleven_recyclerview
 
index c45c5b5..6969dfb 100644 (file)
     <!-- Allows Apollo to read from External Storage -->
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 
+    <!-- Audio Visualizer Permissions -->
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+
     <application
         android:name=".ElevenApplication"
         android:allowBackup="true"
diff --git a/res/drawable/equalizer_background.9.png b/res/drawable/equalizer_background.9.png
new file mode 100644 (file)
index 0000000..255fdc3
Binary files /dev/null and b/res/drawable/equalizer_background.9.png differ
index 1d44c4b..f4e46cf 100644 (file)
             android:layout_width="match_parent"
             android:layout_height="match_parent" />
         <include layout="@layout/loading_empty_container" />
+
+        <View
+            android:id="@+id/equalizerGradient"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/equalizer_background"
+            android:layout_gravity="bottom"/>
+
+        <com.cyngn.eleven.widgets.EqualizerView
+            android:id="@+id/equalizerView"
+            android:gravity="bottom"
+            android:layout_gravity="bottom"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:visibility="visible" />
     </com.cyngn.eleven.widgets.SquareFrame>
 
     <RelativeLayout
diff --git a/res/values-xhdpi/equalizer_config.xml b/res/values-xhdpi/equalizer_config.xml
new file mode 100644 (file)
index 0000000..0208508
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2012-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.
+-->
+<resources>
+    <!-- Height of each filled in block in each eq bar -->
+    <dimen name="eqalizer_path_effect_1">8dp</dimen>
+    <!-- Height of each empty block in each eq bar -->
+    <dimen name="eqalizer_path_effect_2">1dp</dimen>
+    <!-- Width of each eq bar -->
+    <dimen name="eqalizer_path_stroke_width">22dp</dimen>
+
+    <!-- The amount of divisions to make for eq bars -->
+    <integer name="equalizer_divisions">12</integer>
+
+    <!-- fudge factors to tweak display for various configs
+        ends up being dB = ((dB * fuzz_factor) + db_fuzz) -->
+    <integer name="equalizer_db_fuzz_factor">15</integer>
+    <integer name="equalizer_db_fuzz">0</integer>
+</resources>
\ No newline at end of file
diff --git a/res/values-xxhdpi/equalizer_config.xml b/res/values-xxhdpi/equalizer_config.xml
new file mode 100644 (file)
index 0000000..63c0e1b
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2012-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.
+-->
+<resources>
+    <!-- Height of each filled in block in each eq bar -->
+    <dimen name="eqalizer_path_effect_1">8dp</dimen>
+    <!-- Height of each empty block in each eq bar -->
+    <dimen name="eqalizer_path_effect_2">1dp</dimen>
+    <!-- Width of each eq bar -->
+    <dimen name="eqalizer_path_stroke_width">20dp</dimen>
+
+    <!-- The amount of divisions to make for eq bars -->
+    <integer name="equalizer_divisions">16</integer>
+
+    <!-- fudge factors to tweak display for various configs
+        ends up being dB = ((dB * fuzz_factor) + db_fuzz) -->
+    <integer name="equalizer_db_fuzz_factor">35</integer>
+    <integer name="equalizer_db_fuzz">1</integer>
+</resources>
\ No newline at end of file
diff --git a/res/values/equalizer_config.xml b/res/values/equalizer_config.xml
new file mode 100644 (file)
index 0000000..baf5945
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2012-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.
+-->
+<resources>
+    <!-- Height of each filled in block in each eq bar -->
+    <dimen name="eqalizer_path_effect_1">8dp</dimen>
+    <!-- Height of each empty block in each eq bar -->
+    <dimen name="eqalizer_path_effect_2">1dp</dimen>
+    <!-- Width of each eq bar -->
+    <dimen name="eqalizer_path_stroke_width">20dp</dimen>
+
+    <!-- Color for the Equalizer tile -->
+    <color name="equalizer_fill_color">#32ffffff</color>
+
+    <!-- The amount of divisions to make for eq bars -->
+    <integer name="equalizer_divisions">16</integer>
+
+    <!-- fudge factors to tweak display for various configs
+        ends up being dB = (dB * fuzz_factor + db_fuzz) -->
+    <integer name="equalizer_db_fuzz_factor">0</integer>
+    <integer name="equalizer_db_fuzz">0</integer>
+</resources>
\ No newline at end of file
index c6acc4f..fac4e9c 100644 (file)
     <string name="settings_download_only_on_wifi_summary">To reduce carrier charges, don\'t download over mobile networks</string>
     <string name="settings_download_missing_artwork_title">Download missing album art</string>
     <string name="settings_download_artist_images_title">Download missing artist images</string>
+    <string name="settings_general_category">General</string>
+    <string name="settings_show_music_visualization_title">Show music visualization</string>
 
     <!-- App widget -->
     <string name="app_widget_small">Music: 4 \u00d7 1</string>
index f0e7ea0..c2be6a1 100644 (file)
             <!--android:key="download_missing_artist_images"-->
             <!--android:title="@string/settings_download_artist_images_title" />-->
     <!--</PreferenceCategory>-->
+    <PreferenceCategory android:title="@string/settings_general_category" >
+
+        <!-- Music visualizer -->
+        <CheckBoxPreference
+            android:defaultValue="true"
+            android:key="music_visualization"
+            android:title="@string/settings_show_music_visualization_title" />
+    </PreferenceCategory>
     <!-- Storage catetory -->
     <PreferenceCategory android:title="@string/settings_storage_category" >
 
index 717a101..eb9b904 100644 (file)
@@ -28,6 +28,8 @@ import com.cyngn.eleven.utils.ApolloUtils;
 import com.cyngn.eleven.utils.MusicUtils;
 import com.cyngn.eleven.widgets.BlurScrimImage;
 
+import java.util.HashSet;
+
 /**
  * This class is used to display the {@link ViewPager} used to swipe between the
  * main {@link Fragment}s used to browse the user's music.
@@ -43,9 +45,16 @@ public abstract class SlidingPanelActivity extends BaseActivity {
         None,
     }
 
+    public static interface ISlidingPanelListener {
+        public void onBeginSlide();
+        public void onFinishSlide(SlidingPanelActivity.Panel visiblePanel);
+    }
+
     private SlidingUpPanelLayout mFirstPanel;
     private SlidingUpPanelLayout mSecondPanel;
     protected Panel mTargetNavigatePanel;
+    private HashSet<ISlidingPanelListener> mSlidingPanelListeners
+            = new HashSet<ISlidingPanelListener>();
 
     private final ShowPanelClickListener mShowBrowse = new ShowPanelClickListener(Panel.Browse);
     private final ShowPanelClickListener mShowMusicPlayer = new ShowPanelClickListener(Panel.MusicPlayer);
@@ -107,6 +116,8 @@ public abstract class SlidingPanelActivity extends BaseActivity {
                 } else if (slideOffset < 0.75f) {
                     getActionBar().show();
                 }
+
+                onSlide();
             }
 
             @Override
@@ -131,6 +142,8 @@ public abstract class SlidingPanelActivity extends BaseActivity {
                 if (mTargetNavigatePanel == Panel.None) {
                     mFirstPanel.setSlidingEnabled(false);
                 }
+
+                onSlide();
             }
 
             @Override
@@ -213,6 +226,11 @@ public abstract class SlidingPanelActivity extends BaseActivity {
     }
 
     public void showPanel(Panel panel) {
+        // if we are already at our target panel, then don't do anything
+        if (panel == getCurrentPanel()) {
+            return;
+        }
+
         // TODO: Add ability to do this instantaneously as opposed to animate
         switch (panel) {
             case Browse:
@@ -236,13 +254,28 @@ public abstract class SlidingPanelActivity extends BaseActivity {
         }
     }
 
+    protected void onSlide() {
+        for (ISlidingPanelListener listener : mSlidingPanelListeners) {
+            listener.onBeginSlide();
+        }
+    }
+
     /**
      * This checks if we are at our target panel and resets our flag if we are there
      */
     protected void checkTargetNavigation() {
-        if (mTargetNavigatePanel == getCurrentPanel()) {
+        final Panel currentPanel = getCurrentPanel();
+        // This checks if we are at our target panel and resets our flag if we are there
+        if (mTargetNavigatePanel == currentPanel) {
             mTargetNavigatePanel = Panel.None;
         }
+
+        // if we are at the target panel
+        if (mTargetNavigatePanel == Panel.None) {
+            for (ISlidingPanelListener listener : mSlidingPanelListeners) {
+                listener.onFinishSlide(currentPanel);
+            }
+        }
     }
 
     protected Panel getCurrentPanel() {
@@ -309,4 +342,12 @@ public abstract class SlidingPanelActivity extends BaseActivity {
             showPanel(mTargetPanel);
         }
     }
+
+    public void addSlidingPanelListener(final ISlidingPanelListener listener) {
+        mSlidingPanelListeners.add(listener);
+    }
+
+    public void removeSlidingPanelListener(final ISlidingPanelListener listener) {
+        mSlidingPanelListeners.remove(listener);
+    }
 }
index 44690c6..98a1be0 100644 (file)
@@ -10,14 +10,10 @@ import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.media.AudioManager;
-import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
-import android.provider.MediaStore.Audio.Albums;
-import android.provider.MediaStore.Audio.Artists;
-import android.provider.MediaStore.Audio.Playlists;
 import android.support.v4.app.Fragment;
 import android.support.v4.view.ViewPager;
 import android.util.Log;
@@ -41,10 +37,13 @@ import com.cyngn.eleven.loaders.QueueLoader;
 import com.cyngn.eleven.menu.CreateNewPlaylist;
 import com.cyngn.eleven.menu.DeleteDialog;
 import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.ui.activities.SlidingPanelActivity;
 import com.cyngn.eleven.utils.ApolloUtils;
 import com.cyngn.eleven.utils.MusicUtils;
 import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
 import com.cyngn.eleven.widgets.BrowseButton;
+import com.cyngn.eleven.widgets.EqualizerView;
 import com.cyngn.eleven.widgets.LoadingEmptyContainer;
 import com.cyngn.eleven.widgets.NoResultsContainer;
 import com.cyngn.eleven.widgets.PlayPauseProgressButton;
@@ -58,7 +57,8 @@ import java.lang.ref.WeakReference;
 
 import static com.cyngn.eleven.utils.MusicUtils.mService;
 
-public class AudioPlayerFragment extends Fragment implements ServiceConnection {
+public class AudioPlayerFragment extends Fragment implements ServiceConnection,
+        SlidingPanelActivity.ISlidingPanelListener {
     private static final String TAG = AudioPlayerFragment.class.getSimpleName();
 
     /**
@@ -115,6 +115,12 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
     // Total time
     private TextView mTotalTime;
 
+    // Equalizer View
+    private EqualizerView mEqualizerView;
+
+    // Equalizer Gradient
+    private View mEqualizerGradient;
+
     // Broadcast receiver
     private PlaybackStatus mPlaybackStatus;
 
@@ -151,6 +157,9 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
 
         // Initialize the broadcast receiver
         mPlaybackStatus = new PlaybackStatus(this);
+
+        // add a listener for the sliding
+        ((SlidingPanelActivity)getActivity()).addSlidingPanelListener(this);
     }
 
     /**
@@ -166,6 +175,11 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
         initHeaderBar();
 
         initPlaybackControls();
+
+        mEqualizerView = (EqualizerView) mRootView.findViewById(R.id.equalizerView);
+        mEqualizerView.initialize(getActivity());
+        mEqualizerGradient = mRootView.findViewById(R.id.equalizerGradient);
+
         return mRootView;
     }
 
@@ -210,6 +224,8 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
 
         // resumes the update callback for the play pause progress button
         mPlayPauseProgressButton.resume();
+
+        mEqualizerView.onStart();
     }
 
     @Override
@@ -220,6 +236,8 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
         mPlayPauseProgressButton.pause();
 
         mImageFetcher.flush();
+
+        mEqualizerView.onStop();
     }
 
     @Override
@@ -234,6 +252,8 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
             mToken = null;
         }
 
+        ((SlidingPanelActivity)getActivity()).removeSlidingPanelListener(this);
+
         // Unregister the receiver
         try {
             getActivity().unregisterReceiver(mPlaybackStatus);
@@ -423,10 +443,18 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
         if(queueSize == 0) {
             mAlbumArtViewPager.setVisibility(View.GONE);
             mQueueEmpty.showNoResults();
+            mEqualizerGradient.setVisibility(View.GONE);
+            mEqualizerView.checkStateChanged();
             mAddToPlaylistButton.setVisibility(View.GONE);
         } else {
             mAlbumArtViewPager.setVisibility(View.VISIBLE);
             mQueueEmpty.hideAll();
+            if (PreferenceUtils.getInstance(getActivity()).getShowVisualizer()) {
+                mEqualizerGradient.setVisibility(View.VISIBLE);
+            } else {
+                mEqualizerGradient.setVisibility(View.GONE);
+            }
+            mEqualizerView.checkStateChanged();
             mAddToPlaylistButton.setVisibility(View.VISIBLE);
         }
     }
@@ -657,6 +685,18 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
         return super.onContextItemSelected(item);
     }
 
+    @Override
+    public void onBeginSlide() {
+        mEqualizerView.setPanelVisible(false);
+    }
+
+    @Override
+    public void onFinishSlide(SlidingPanelActivity.Panel visiblePanel) {
+        if (visiblePanel == SlidingPanelActivity.Panel.MusicPlayer) {
+            mEqualizerView.setPanelVisible(true);
+        }
+    }
+
     /**
      * Used to update the current time string
      */
index 903371f..61351cc 100644 (file)
@@ -69,6 +69,9 @@ public final class PreferenceUtils {
     // datetime cutoff for determining which songs go in last added playlist
     public static final String LAST_ADDED_CUTOFF = "last_added_cutoff";
 
+    // show visualizer flag
+    public static final String SHOW_VISUALIZER = "music_visualization";
+
     private static PreferenceUtils sInstance;
 
     private final SharedPreferences mPreferences;
@@ -303,4 +306,8 @@ public final class PreferenceUtils {
     public long getLastAddedCutoff() {
         return mPreferences.getLong(LAST_ADDED_CUTOFF, 0L);
     }
+
+    public boolean getShowVisualizer() {
+        return mPreferences.getBoolean(SHOW_VISUALIZER, true);
+    }
 }
diff --git a/src/com/cyngn/eleven/widgets/EqualizerView.java b/src/com/cyngn/eleven/widgets/EqualizerView.java
new file mode 100644 (file)
index 0000000..5814692
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+import com.pheelicks.visualizer.AudioData;
+import com.pheelicks.visualizer.FFTData;
+import com.pheelicks.visualizer.VisualizerView;
+import com.pheelicks.visualizer.renderer.Renderer;
+
+public class EqualizerView extends VisualizerView {
+    private boolean mLinked = false;
+    private boolean mStarted = false;
+    private boolean mPanelVisible = false;
+
+    private final Runnable mLinkVisualizer = new Runnable() {
+        @Override
+        public void run() {
+            if (!mLinked) {
+                animate().alpha(1).setDuration(300);
+                link(0);
+                mLinked = true;
+            }
+        }
+    };
+
+    private final Runnable mUnlinkVisualizer = new Runnable() {
+        @Override
+        public void run() {
+            if (mLinked) {
+                animate().alpha(0).setDuration(300);
+                unlink();
+                mLinked = false;
+            }
+        }
+    };
+
+    private static class TileBarGraphRenderer extends Renderer {
+        private int mDivisions;
+        private Paint mPaint;
+        private int mDbFuzz;
+        private int mDbFuzzFactor;
+
+        /**
+         * Renders the FFT data as a series of lines, in histogram form
+         *
+         * @param divisions - must be a power of 2. Controls how many lines to draw
+         * @param paint     - Paint to draw lines with
+         * @param dbfuzz    - final dB display adjustment
+         * @param dbFactor  - dbfuzz is multiplied by dbFactor.
+         */
+        public TileBarGraphRenderer(int divisions, Paint paint, int dbfuzz, int dbFactor) {
+            super();
+            mDivisions = divisions;
+            mPaint = paint;
+            mDbFuzz = dbfuzz;
+            mDbFuzzFactor = dbFactor;
+        }
+
+        @Override
+        public void onRender(Canvas canvas, AudioData data, Rect rect) {
+            // Do nothing, we only display FFT data
+        }
+
+        @Override
+        public void onRender(Canvas canvas, FFTData data, Rect rect) {
+            for (int i = 0; i < data.bytes.length / mDivisions; i++) {
+                mFFTPoints[i * 4] = i * 4 * mDivisions;
+                mFFTPoints[i * 4 + 2] = i * 4 * mDivisions;
+                byte rfk = data.bytes[mDivisions * i];
+                byte ifk = data.bytes[mDivisions * i + 1];
+                float magnitude = (rfk * rfk + ifk * ifk);
+                int dbValue = (int) (10 * Math.log10(magnitude));
+
+                mFFTPoints[i * 4 + 1] = rect.height();
+                mFFTPoints[i * 4 + 3] = rect.height() - (dbValue * mDbFuzzFactor + mDbFuzz);
+            }
+
+            canvas.drawLines(mFFTPoints, mPaint);
+        }
+    }
+
+    public EqualizerView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public EqualizerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public EqualizerView(Context context) {
+        this(context, null, 0);
+    }
+
+    public void initialize(Context context) {
+        setEnabled(false);
+
+        Resources res = mContext.getResources();
+        Paint paint = new Paint();
+        paint.setStrokeWidth(res.getDimensionPixelSize(R.dimen.eqalizer_path_stroke_width));
+        paint.setAntiAlias(true);
+        paint.setColor(res.getColor(R.color.equalizer_fill_color));
+        paint.setPathEffect(new DashPathEffect(new float[]{
+                res.getDimensionPixelSize(R.dimen.eqalizer_path_effect_1),
+                res.getDimensionPixelSize(R.dimen.eqalizer_path_effect_2)
+        }, 0));
+
+        int bars = res.getInteger(R.integer.equalizer_divisions);
+        addRenderer(new TileBarGraphRenderer(bars, paint,
+                res.getInteger(R.integer.equalizer_db_fuzz),
+                res.getInteger(R.integer.equalizer_db_fuzz_factor)));
+    }
+
+    /**
+     * Follows Fragment onStart to determine if the containing fragment/activity is started
+     */
+    public void onStart() {
+        mStarted = true;
+        checkStateChanged();
+    }
+
+    /**
+     * Follows Fragment onStop to determine if the containing fragment/activity is stopped
+     */
+    public void onStop() {
+        mStarted = false;
+        checkStateChanged();
+    }
+
+    /**
+     * Separate method to toggle panel visibility - currently used when the user slides to
+     * improve performance of the sliding panel
+     */
+    public void setPanelVisible(boolean panelVisible) {
+        if (mPanelVisible != panelVisible) {
+            mPanelVisible = panelVisible;
+            checkStateChanged();
+        }
+    }
+
+    /**
+     * Checks the state of the EqualizerView to determine whether we want to link up the equalizer
+     */
+    public void checkStateChanged() {
+        if (mPanelVisible && mStarted
+                && PreferenceUtils.getInstance(mContext).getShowVisualizer()
+                && MusicUtils.getQueueSize() > 0) {
+            mLinkVisualizer.run();
+        } else {
+            mUnlinkVisualizer.run();
+        }
+    }
+}