From d122105b927898d2e9b8a3c93d7d497f85b3910b Mon Sep 17 00:00:00 2001
From: Joshua Bahnsen <archrival@gmail.com>
Date: Wed, 22 Jan 2014 01:58:38 -0700
Subject: [PATCH] Add image concurrency settings, stop jukebox and ImageLoader
 threads on Exit, show settings screen after welcome dialog, only start
 jukebox thread when jukebox is enabled, version 1.3.0.4

---
 AndroidManifest.xml                           |  4 +-
 res/values-fr/strings.xml                     | 13 +++++
 res/values-hu/strings.xml                     | 13 +++++
 res/values/arrays.xml                         | 28 ++++++++++
 res/values/strings.xml                        | 13 +++++
 res/xml/settings.xml                          |  6 ++
 .../androidapp/activity/MainActivity.java     |  9 ++-
 .../androidapp/activity/ResultActivity.java   |  4 +-
 .../androidapp/activity/SettingsActivity.java |  3 +
 .../activity/SubsonicTabActivity.java         | 15 +++--
 .../androidapp/service/DownloadService.java   |  4 ++
 .../service/DownloadServiceImpl.java          | 23 ++++++++
 .../androidapp/service/JukeboxService.java    | 56 ++++++++++++++++---
 .../ultrasonic/androidapp/util/Constants.java |  1 +
 .../androidapp/util/ImageLoader.java          | 47 +++++++++++++---
 .../ultrasonic/androidapp/util/Util.java      | 23 +++++++-
 16 files changed, 237 insertions(+), 25 deletions(-)

diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f4c60a18..c1000c8a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2,8 +2,8 @@
 <manifest xmlns:a="http://schemas.android.com/apk/res/android"
           package="com.thejoshwa.ultrasonic.androidapp"
           a:installLocation="auto"
-          a:versionCode="43"
-          a:versionName="1.3.0.3">
+          a:versionCode="44"
+          a:versionName="1.3.0.4">
 
 <uses-permission a:name="android.permission.INTERNET"/>
     <uses-permission a:name="android.permission.READ_PHONE_STATE"/>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index f347b258..c1193303 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -374,6 +374,19 @@
     <string name="download.menu_show_artist">Afficher l\'artiste</string>
     <string name="settings.scan_media">Scan Media After Download</string>
     <string name="settings.scan_media_summary">Automatically scan media after download</string>
+    <string name="settings.image_loader_concurrency">Image Loader Concurrency</string>
+    <string name="settings.image_loader_concurrency_1">1</string>
+    <string name="settings.image_loader_concurrency_2">2</string>
+    <string name="settings.image_loader_concurrency_3">3</string>
+    <string name="settings.image_loader_concurrency_4">4</string>
+    <string name="settings.image_loader_concurrency_5">5</string>
+    <string name="settings.image_loader_concurrency_6">6</string>
+    <string name="settings.image_loader_concurrency_7">7</string>
+    <string name="settings.image_loader_concurrency_8">8</string>
+    <string name="settings.image_loader_concurrency_9">9</string>
+    <string name="settings.image_loader_concurrency_10">10</string>
+    <string name="settings.image_loader_concurrency_11">11</string>
+    <string name="settings.image_loader_concurrency_12">12</string>
 
     <plurals name="select_album_n_songs">
         <item quantity="zero">Aucun titre</item>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 71d3210b..46d0cc20 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -374,6 +374,19 @@
     <string name="download.menu_show_artist">Ugrás az előadóhoz</string>
     <string name="settings.scan_media">Scan Media After Download</string>
     <string name="settings.scan_media_summary">Automatically scan media after download</string>
+    <string name="settings.image_loader_concurrency">Image Loader Concurrency</string>
+    <string name="settings.image_loader_concurrency_1">1</string>
+    <string name="settings.image_loader_concurrency_2">2</string>
+    <string name="settings.image_loader_concurrency_3">3</string>
+    <string name="settings.image_loader_concurrency_4">4</string>
+    <string name="settings.image_loader_concurrency_5">5</string>
+    <string name="settings.image_loader_concurrency_6">6</string>
+    <string name="settings.image_loader_concurrency_7">7</string>
+    <string name="settings.image_loader_concurrency_8">8</string>
+    <string name="settings.image_loader_concurrency_9">9</string>
+    <string name="settings.image_loader_concurrency_10">10</string>
+    <string name="settings.image_loader_concurrency_11">11</string>
+    <string name="settings.image_loader_concurrency_12">12</string>
 
     <plurals name="select_album_n_songs">
         <item quantity="zero">Nincsenek dalok</item>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index a7e3feee..56d6ecb8 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -263,5 +263,33 @@
         <item>@string/settings.share_hours</item>
         <item>@string/settings.share_days</item>
     </string-array>
+    <string-array name="imageConcurrencyNames">
+        <item>@string/settings.image_loader_concurrency_1</item>
+        <item>@string/settings.image_loader_concurrency_2</item>
+        <item>@string/settings.image_loader_concurrency_3</item>
+        <item>@string/settings.image_loader_concurrency_4</item>
+        <item>@string/settings.image_loader_concurrency_5</item>
+        <item>@string/settings.image_loader_concurrency_6</item>
+        <item>@string/settings.image_loader_concurrency_7</item>
+        <item>@string/settings.image_loader_concurrency_8</item>
+        <item>@string/settings.image_loader_concurrency_9</item>
+        <item>@string/settings.image_loader_concurrency_10</item>
+        <item>@string/settings.image_loader_concurrency_11</item>
+        <item>@string/settings.image_loader_concurrency_12</item>
+    </string-array>
+    <string-array name="imageConcurrencyValues">
+        <item>1</item>
+        <item>2</item>
+        <item>3</item>
+        <item>4</item>
+        <item>5</item>
+        <item>6</item>
+        <item>7</item>
+        <item>8</item>
+        <item>9</item>
+        <item>10</item>
+        <item>11</item>
+        <item>12</item>
+    </string-array>
 
 </resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f1ec7eea..61d9d5fa 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -374,6 +374,19 @@
     <string name="download.menu_show_artist">Show Artist</string>
     <string name="settings.scan_media">Scan Media After Download</string>
     <string name="settings.scan_media_summary">Automatically scan media after download</string>
+    <string name="settings.image_loader_concurrency">Image Loader Concurrency</string>
+    <string name="settings.image_loader_concurrency_1">1</string>
+    <string name="settings.image_loader_concurrency_2">2</string>
+    <string name="settings.image_loader_concurrency_3">3</string>
+    <string name="settings.image_loader_concurrency_4">4</string>
+    <string name="settings.image_loader_concurrency_5">5</string>
+    <string name="settings.image_loader_concurrency_6">6</string>
+    <string name="settings.image_loader_concurrency_7">7</string>
+    <string name="settings.image_loader_concurrency_8">8</string>
+    <string name="settings.image_loader_concurrency_9">9</string>
+    <string name="settings.image_loader_concurrency_10">10</string>
+    <string name="settings.image_loader_concurrency_11">11</string>
+    <string name="settings.image_loader_concurrency_12">12</string>
 
     <plurals name="select_album_n_songs">
         <item quantity="zero">No songs</item>
diff --git a/res/xml/settings.xml b/res/xml/settings.xml
index c2e89b19..cf5b0bee 100644
--- a/res/xml/settings.xml
+++ b/res/xml/settings.xml
@@ -49,6 +49,12 @@
             a:entryValues="@array/viewRefreshValues"
             a:key="viewRefresh"
             a:title="@string/settings.view_refresh"/>
+        <ListPreference
+            a:defaultValue="5"
+            a:entries="@array/imageConcurrencyNames"
+            a:entryValues="@array/imageConcurrencyValues"
+            a:key="imageLoaderConcurrency"
+            a:title="@string/settings.image_loader_concurrency"/>
     </PreferenceCategory>
     <PreferenceCategory
         a:title="@string/settings.playback_control_title"
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/activity/MainActivity.java b/src/com/thejoshwa/ultrasonic/androidapp/activity/MainActivity.java
index 6b0f6d90..8acae96d 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/activity/MainActivity.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/activity/MainActivity.java
@@ -75,6 +75,13 @@ public class MainActivity extends SubsonicTabActivity
 		if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT))
 		{
 			setResult(Constants.RESULT_CLOSE_ALL);
+			getDownloadService().stopJukeboxService();
+
+			if (getImageLoader() != null)
+			{
+				getImageLoader().stopImageLoader();
+			}
+
 			finish();
 			exit();
 			return;
@@ -471,7 +478,7 @@ public class MainActivity extends SubsonicTabActivity
 
 			if (show || Util.getRestUrl(this, null).contains("yourhost"))
 			{
-				Util.info(this, R.string.main_welcome_title, R.string.main_welcome_text);
+				Util.showWelcomeDialog(this, this, R.string.main_welcome_title, R.string.main_welcome_text);
 			}
 		}
 	}
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/activity/ResultActivity.java b/src/com/thejoshwa/ultrasonic/androidapp/activity/ResultActivity.java
index f14d1ad7..44ed0c3d 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/activity/ResultActivity.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/activity/ResultActivity.java
@@ -23,12 +23,12 @@ public class ResultActivity extends Activity
 		super.onActivityResult(requestCode, resultCode, data);
 	}
 
-	protected void startActivityForResultWithoutTransition(Activity currentActivity, Class<? extends Activity> newActivity)
+	public void startActivityForResultWithoutTransition(Activity currentActivity, Class<? extends Activity> newActivity)
 	{
 		startActivityForResultWithoutTransition(currentActivity, new Intent(currentActivity, newActivity));
 	}
 
-	protected void startActivityForResultWithoutTransition(Activity currentActivity, Intent intent)
+	public void startActivityForResultWithoutTransition(Activity currentActivity, Intent intent)
 	{
 		startActivityForResult(intent, 0);
 		Util.disablePendingTransition(currentActivity);
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/activity/SettingsActivity.java b/src/com/thejoshwa/ultrasonic/androidapp/activity/SettingsActivity.java
index b9239923..5dc04b3e 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/activity/SettingsActivity.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/activity/SettingsActivity.java
@@ -88,6 +88,7 @@ public class SettingsActivity extends PreferenceResultActivity implements Shared
 	private CheckBoxPreference sendBluetoothNotifications;
 	private CheckBoxPreference sendBluetoothAlbumArt;
 	private ListPreference viewRefresh;
+	private ListPreference imageLoaderConcurrency;
 	private EditTextPreference sharingDefaultDescription;
 	private EditTextPreference sharingDefaultGreeting;
 	private TimeSpanPreference sharingDefaultExpiration;
@@ -181,6 +182,7 @@ public class SettingsActivity extends PreferenceResultActivity implements Shared
 		sendBluetoothAlbumArt = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART);
 		sendBluetoothNotifications = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS);
 		viewRefresh = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIEW_REFRESH);
+		imageLoaderConcurrency = (ListPreference) findPreference(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY);
 		sharingDefaultDescription = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION);
 		sharingDefaultGreeting = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING);
 		sharingDefaultExpiration = (TimeSpanPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION);
@@ -542,6 +544,7 @@ public class SettingsActivity extends PreferenceResultActivity implements Shared
 		chatRefreshInterval.setSummary(chatRefreshInterval.getEntry());
 		directoryCacheTime.setSummary(directoryCacheTime.getEntry());
 		viewRefresh.setSummary(viewRefresh.getEntry());
+		imageLoaderConcurrency.setSummary(imageLoaderConcurrency.getEntry());
 		sharingDefaultExpiration.setSummary(sharingDefaultExpiration.getText());
 		sharingDefaultDescription.setSummary(sharingDefaultDescription.getText());
 		sharingDefaultGreeting.setSummary(sharingDefaultGreeting.getText());
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/activity/SubsonicTabActivity.java b/src/com/thejoshwa/ultrasonic/androidapp/activity/SubsonicTabActivity.java
index a0bb09da..82847ddd 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/activity/SubsonicTabActivity.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/activity/SubsonicTabActivity.java
@@ -94,7 +94,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
 {
 	private static final String TAG = SubsonicTabActivity.class.getSimpleName();
 	private static final Pattern COMPILE = Pattern.compile(":");
-	private static ImageLoader IMAGE_LOADER;
+	protected static ImageLoader IMAGE_LOADER;
 	protected static String theme;
 	private static SubsonicTabActivity instance;
 
@@ -228,7 +228,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
 		super.onDestroy();
 		destroyed = true;
 		nowPlayingView = null;
-		getImageLoader().clear();
+		clearImageLoader();
 	}
 
 	@Override
@@ -957,12 +957,19 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
 		}
 	}
 
+	public synchronized void clearImageLoader()
+	{
+		if (IMAGE_LOADER != null && IMAGE_LOADER.isRunning()) IMAGE_LOADER.clear();
+	}
+
 	public synchronized ImageLoader getImageLoader()
 	{
-		if (IMAGE_LOADER == null)
+		if (IMAGE_LOADER == null || !IMAGE_LOADER.isRunning())
 		{
-			IMAGE_LOADER = new ImageLoader(this);
+			IMAGE_LOADER = new ImageLoader(this, Util.getImageLoaderConcurrency(this));
+			IMAGE_LOADER.startImageLoader();
 		}
+
 		return IMAGE_LOADER;
 	}
 
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/service/DownloadService.java b/src/com/thejoshwa/ultrasonic/androidapp/service/DownloadService.java
index fb66a56a..c4e62da5 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/service/DownloadService.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/service/DownloadService.java
@@ -140,4 +140,8 @@ public interface DownloadService
 	void swap(boolean mainList, int from, int to);
 
 	void restore(List<Entry> songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist);
+
+	void stopJukeboxService();
+
+	void startJukeboxService();
 }
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/service/DownloadServiceImpl.java b/src/com/thejoshwa/ultrasonic/androidapp/service/DownloadServiceImpl.java
index 4995b63c..4d370391 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/service/DownloadServiceImpl.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/service/DownloadServiceImpl.java
@@ -466,6 +466,18 @@ public class DownloadServiceImpl extends Service implements DownloadService
 		}
 	}
 
+	@Override
+	public void stopJukeboxService()
+	{
+		jukeboxService.stopJukeboxService();
+	}
+
+	@Override
+	public void startJukeboxService()
+	{
+		jukeboxService.startJukeboxService();
+	}
+
 	@Override
 	public synchronized void setShufflePlayEnabled(boolean enabled)
 	{
@@ -1414,8 +1426,11 @@ public class DownloadServiceImpl extends Service implements DownloadService
 	{
 		this.jukeboxEnabled = jukeboxEnabled;
 		jukeboxService.setEnabled(jukeboxEnabled);
+
 		if (jukeboxEnabled)
 		{
+			jukeboxService.startJukeboxService();
+
 			reset();
 
 			// Cancel current download, if necessary.
@@ -1424,6 +1439,10 @@ public class DownloadServiceImpl extends Service implements DownloadService
 				currentDownloading.cancelDownload();
 			}
 		}
+		else
+		{
+			jukeboxService.stopJukeboxService();
+		}
 	}
 
 	@Override
@@ -1505,6 +1524,10 @@ public class DownloadServiceImpl extends Service implements DownloadService
 			audioManager.unregisterRemoteControlClient(remoteControlClient);
 			audioManager.registerRemoteControlClient(remoteControlClient);
 		}
+		else
+		{
+			setUpRemoteControlClient();
+		}
 
 		Log.i(TAG, String.format("In updateRemoteControl, playerState: %s [%d]", playerState, getPlayerPosition()));
 
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/service/JukeboxService.java b/src/com/thejoshwa/ultrasonic/androidapp/service/JukeboxService.java
index dbaa184b..f78834de 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/service/JukeboxService.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/service/JukeboxService.java
@@ -64,6 +64,8 @@ public class JukeboxService
 	private JukeboxStatus jukeboxStatus;
 	private float gain = 0.5f;
 	private VolumeToast volumeToast;
+	private boolean running = false;
+	private Thread serviceThread;
 
 	// TODO: Report warning if queue fills up.
 	// TODO: Create shutdown method?
@@ -74,19 +76,42 @@ public class JukeboxService
 	public JukeboxService(DownloadServiceImpl downloadService)
 	{
 		this.downloadService = downloadService;
-		new Thread()
+	}
+
+	public void startJukeboxService()
+	{
+		if (running) return;
+
+		running = true;
+		startProcessTasks();
+	}
+
+	public void stopJukeboxService()
+	{
+		running = false;
+		Util.sleepQuietly(1000);
+
+		if (serviceThread != null) serviceThread.interrupt();
+	}
+
+	private void startProcessTasks()
+	{
+		serviceThread = new Thread()
 		{
 			@Override
 			public void run()
 			{
 				processTasks();
 			}
-		}.start();
+		};
+
+		serviceThread.start();
 	}
 
 	private synchronized void startStatusUpdate()
 	{
 		stopStatusUpdate();
+
 		Runnable updateTask = new Runnable()
 		{
 			@Override
@@ -96,6 +121,7 @@ public class JukeboxService
 				tasks.add(new GetStatus());
 			}
 		};
+
 		statusUpdateFuture = executorService.scheduleWithFixedDelay(updateTask, STATUS_UPDATE_INTERVAL_SECONDS, STATUS_UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS);
 	}
 
@@ -110,9 +136,10 @@ public class JukeboxService
 
 	private void processTasks()
 	{
-		while (true)
+		while (running)
 		{
 			JukeboxTask task = null;
+
 			try
 			{
 				if (!Util.isOffline(downloadService))
@@ -121,11 +148,17 @@ public class JukeboxService
 					JukeboxStatus status = task.execute();
 					onStatusUpdate(status);
 				}
+			}
+			catch (InterruptedException ignored)
+			{
+
 			}
 			catch (Throwable x)
 			{
 				onError(task, x);
 			}
+
+			Util.sleepQuietly(1);
 		}
 	}
 
@@ -136,6 +169,7 @@ public class JukeboxService
 
 		// Track change?
 		Integer index = jukeboxStatus.getCurrentPlayingIndex();
+
 		if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex())
 		{
 			downloadService.setCurrentPlaying(index);
@@ -165,6 +199,7 @@ public class JukeboxService
 	private void disableJukeboxOnError(Throwable x, final int resourceId)
 	{
 		Log.w(TAG, x.toString());
+
 		handler.post(new Runnable()
 		{
 			@Override
@@ -173,6 +208,7 @@ public class JukeboxService
 				Util.toast(downloadService, resourceId, false);
 			}
 		});
+
 		downloadService.setJukeboxEnabled(false);
 	}
 
@@ -187,6 +223,7 @@ public class JukeboxService
 		{
 			ids.add(file.getSong().getId());
 		}
+
 		tasks.add(new SetPlaylist(ids));
 	}
 
@@ -213,6 +250,7 @@ public class JukeboxService
 		tasks.remove(Start.class);
 
 		stopStatusUpdate();
+
 		tasks.add(new Stop());
 	}
 
@@ -266,17 +304,19 @@ public class JukeboxService
 	public void setEnabled(boolean enabled)
 	{
 		tasks.clear();
+
 		if (enabled)
 		{
 			updatePlaylist();
 		}
+
 		stop();
+
 		downloadService.setPlayerState(PlayerState.IDLE);
 	}
 
 	private static class TaskQueue
 	{
-
 		private final LinkedBlockingQueue<JukeboxTask> queue = new LinkedBlockingQueue<JukeboxTask>();
 
 		void add(JukeboxTask jukeboxTask)
@@ -289,15 +329,17 @@ public class JukeboxService
 			return queue.take();
 		}
 
-		void remove(Class<? extends JukeboxTask> clazz)
+		void remove(Class<? extends JukeboxTask> taskClass)
 		{
 			try
 			{
 				Iterator<JukeboxTask> iterator = queue.iterator();
+
 				while (iterator.hasNext())
 				{
 					JukeboxTask task = iterator.next();
-					if (clazz.equals(task.getClass()))
+
+					if (taskClass.equals(task.getClass()))
 					{
 						iterator.remove();
 					}
@@ -317,7 +359,6 @@ public class JukeboxService
 
 	private abstract class JukeboxTask
 	{
-
 		abstract JukeboxStatus execute() throws Exception;
 
 		@Override
@@ -338,7 +379,6 @@ public class JukeboxService
 
 	private class SetPlaylist extends JukeboxTask
 	{
-
 		private final List<String> ids;
 
 		SetPlaylist(List<String> ids)
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/util/Constants.java b/src/com/thejoshwa/ultrasonic/androidapp/util/Constants.java
index 15058088..ac959abe 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/util/Constants.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/util/Constants.java
@@ -128,6 +128,7 @@ public final class Constants
 	public static final String PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION = "sharingDefaultExpiration";
 	public static final String PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist";
 	public static final String PREFERENCES_KEY_SCAN_MEDIA = "scanMedia";
+	public static final String PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY = "imageLoaderConcurrency";
 
 	// Name of the preferences file.
 	public static final String PREFERENCES_FILE_NAME = "com.thejoshwa.ultrasonic.androidapp_preferences";
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/util/ImageLoader.java b/src/com/thejoshwa/ultrasonic/androidapp/util/ImageLoader.java
index 56585e48..ba2435ac 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/util/ImageLoader.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/util/ImageLoader.java
@@ -36,6 +36,8 @@ import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
 import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
 import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 
@@ -50,7 +52,6 @@ public class ImageLoader implements Runnable
 {
 
 	private static final String TAG = ImageLoader.class.getSimpleName();
-	private static final int CONCURRENCY = 5;
 
 	private final LRUCache<String, Bitmap> cache = new LRUCache<String, Bitmap>(150);
 	private final BlockingQueue<Task> queue;
@@ -58,10 +59,14 @@ public class ImageLoader implements Runnable
 	private final int imageSizeLarge;
 	private Bitmap largeUnknownImage;
 	private Context context;
+	private Collection<Thread> threads = new ArrayList<Thread>();
+	private boolean running = false;
+	private int concurrency;
 
-	public ImageLoader(Context context)
+	public ImageLoader(Context context, int concurrency)
 	{
 		this.context = context;
+		this.concurrency = concurrency;
 		queue = new LinkedBlockingQueue<Task>(1000);
 
 		Resources resources = context.getResources();
@@ -76,12 +81,36 @@ public class ImageLoader implements Runnable
 		DisplayMetrics metrics = context.getResources().getDisplayMetrics();
 		imageSizeLarge = Math.round(Math.min(metrics.widthPixels, metrics.heightPixels));
 
-		for (int i = 0; i < CONCURRENCY; i++)
+		createLargeUnknownImage(context);
+	}
+
+	public synchronized boolean isRunning()
+	{
+		return running && !threads.isEmpty();
+	}
+
+	public void startImageLoader()
+	{
+		running = true;
+
+		for (int i = 0; i < this.concurrency; i++)
 		{
-			new Thread(this, "ImageLoader").start();
+			Thread thread = new Thread(this, "ImageLoader");
+			threads.add(thread);
+			thread.start();
+		}
+	}
+
+	public synchronized void stopImageLoader()
+	{
+		clear();
+
+		for (Thread thread : threads)
+		{
+			thread.interrupt();
 		}
 
-		createLargeUnknownImage(context);
+		running = false;
 	}
 
 	private void createLargeUnknownImage(Context context)
@@ -131,7 +160,7 @@ public class ImageLoader implements Runnable
 
 	private static String getKey(String coverArtId, int size)
 	{
-		return coverArtId + ":" + size;
+		return String.format("%s:%d", coverArtId, size);
 	}
 
 	public Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size)
@@ -226,12 +255,16 @@ public class ImageLoader implements Runnable
 	@Override
 	public void run()
 	{
-		while (true)
+		while (running)
 		{
 			try
 			{
 				Task task = queue.take();
 				task.execute();
+			}
+			catch (InterruptedException ignored)
+			{
+
 			}
 			catch (Throwable x)
 			{
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/util/Util.java b/src/com/thejoshwa/ultrasonic/androidapp/util/Util.java
index fc478a01..27470e31 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/util/Util.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/util/Util.java
@@ -52,6 +52,7 @@ import android.widget.Toast;
 import com.thejoshwa.ultrasonic.androidapp.R;
 import com.thejoshwa.ultrasonic.androidapp.activity.DownloadActivity;
 import com.thejoshwa.ultrasonic.androidapp.activity.MainActivity;
+import com.thejoshwa.ultrasonic.androidapp.activity.SettingsActivity;
 import com.thejoshwa.ultrasonic.androidapp.domain.Bookmark;
 import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
 import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory.Entry;
@@ -127,7 +128,7 @@ public class Util extends DownloadActivity
 
 	public static boolean isOffline(Context context)
 	{
-		return context != null && getActiveServer(context) == 0;
+		return context == null || getActiveServer(context) == 0;
 	}
 
 	public static boolean isScreenLitOnDownload(Context context)
@@ -257,6 +258,7 @@ public class Util extends DownloadActivity
 		{
 			return false;
 		}
+
 		SharedPreferences preferences = getPreferences(context);
 		return preferences.getBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + instance, false);
 	}
@@ -806,6 +808,19 @@ public class Util extends DownloadActivity
 		showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId);
 	}
 
+	public static void showWelcomeDialog(final Context context, final MainActivity activity, int titleId, int messageId)
+	{
+		new AlertDialog.Builder(context).setIcon(android.R.drawable.ic_dialog_info).setTitle(titleId).setMessage(messageId).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
+		{
+			@Override
+			public void onClick(DialogInterface dialog, int i)
+			{
+				dialog.dismiss();
+				activity.startActivityForResultWithoutTransition(activity, SettingsActivity.class);
+			}
+		}).show();
+	}
+
 	private static void showDialog(Context context, int icon, int titleId, int messageId)
 	{
 		new AlertDialog.Builder(context).setIcon(icon).setTitle(titleId).setMessage(messageId).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
@@ -1631,4 +1646,10 @@ public class Util extends DownloadActivity
 		Intent scanFileIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri);
 		context.sendBroadcast(scanFileIntent);
 	}
+
+	public static int getImageLoaderConcurrency(Context context)
+	{
+		SharedPreferences preferences = getPreferences(context);
+		return Integer.parseInt(preferences.getString(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY, "5"));
+	}
 }