From f8e201cb80902a857a66d620ee81988f086b7952 Mon Sep 17 00:00:00 2001
From: casey langen <casey.langen@gmail.com>
Date: Wed, 17 May 2017 23:15:59 -0700
Subject: [PATCH] - fixed core library external id generation (use uuids
 instead of an   auto-incrementing int) - ensure the web server plugin sends
 'external_id' to clients - allow user to configure streaming disk cache size
 on the android   client - some other android client ui cleanup

---
 src/contrib/websocket_remote/Constants.h      |   1 +
 src/contrib/websocket_remote/HttpServer.cpp   |  18 +-
 .../websocket_remote/WebSocketServer.cpp      |   1 +
 src/core/library/Indexer.cpp                  |  14 -
 src/core/library/LocalLibrary.cpp             |  11 +-
 src/core/library/track/IndexerTrack.cpp       |   9 +-
 .../remote/playback/ExoPlayerWrapper.java     |  56 ++--
 .../playback/StreamingPlaybackService.java    |   9 +-
 .../remote/ui/activity/SettingsActivity.java  |  99 ++++--
 .../src/main/res/layout/activity_settings.xml | 307 +++++++++---------
 .../app/src/main/res/menu/settings_menu.xml   |  11 +
 .../app/src/main/res/values/disk_cache.xml    |  11 +
 .../app/src/main/res/values/strings.xml       |   5 +-
 13 files changed, 322 insertions(+), 230 deletions(-)
 create mode 100644 src/musikdroid/app/src/main/res/menu/settings_menu.xml
 create mode 100644 src/musikdroid/app/src/main/res/values/disk_cache.xml

diff --git a/src/contrib/websocket_remote/Constants.h b/src/contrib/websocket_remote/Constants.h
index 306322f3d..9ad030703 100644
--- a/src/contrib/websocket_remote/Constants.h
+++ b/src/contrib/websocket_remote/Constants.h
@@ -76,6 +76,7 @@ namespace key {
     static const std::string playing_current_time = "playing_current_time";
     static const std::string playing_track = "playing_track";
     static const std::string title = "title";
+    static const std::string external_id = "external_id";
     static const std::string filename = "filename";
     static const std::string artist = "artist";
     static const std::string album = "album";
diff --git a/src/contrib/websocket_remote/HttpServer.cpp b/src/contrib/websocket_remote/HttpServer.cpp
index 37affaecb..ad1326e98 100644
--- a/src/contrib/websocket_remote/HttpServer.cpp
+++ b/src/contrib/websocket_remote/HttpServer.cpp
@@ -288,15 +288,16 @@ int HttpServer::HandleRequest(
         if (parts.size() > 0) {
             if (parts.at(0) == fragment::audio && parts.size() == 3) {
                 IRetainedTrack* track = nullptr;
+                bool byExternalId = (parts.at(1) == fragment::external_id);
 
-                if (parts.at(1) == fragment::id) {
-                    uint64_t id = std::stoull(urlDecode(parts.at(2)));
-                    track = server->context.dataProvider->QueryTrackById(id);
-                }
-                else if (parts.at(1) == fragment::external_id) {
+                if (byExternalId) {
                     std::string externalId = urlDecode(parts.at(2));
                     track = server->context.dataProvider->QueryTrackByExternalId(externalId.c_str());
                 }
+                else if (parts.at(1) == fragment::id) {
+                    uint64_t id = std::stoull(urlDecode(parts.at(2)));
+                    track = server->context.dataProvider->QueryTrackById(id);
+                }
 
                 if (track) {
                     std::string filename = GetMetadataString(track, key::filename);
@@ -360,6 +361,13 @@ int HttpServer::HandleRequest(
                                 MHD_add_response_header(response, "Accept-Ranges", "bytes");
                             }
 
+                            if (byExternalId) {
+                                /* if we're using an on-demand transcoder, ensure the client does not cache the
+                                result because we have to guess the content length. */
+                                std::string value = isOnDemandTranscoder ? "no-cache" : "public, max-age=31536000";
+                                MHD_add_response_header(response, "Cache-Control", value.c_str());
+                            }
+
                             MHD_add_response_header(response, "Content-Type", contentType(filename).c_str());
                             MHD_add_response_header(response, "Server", "musikcube websocket_remote");
 
diff --git a/src/contrib/websocket_remote/WebSocketServer.cpp b/src/contrib/websocket_remote/WebSocketServer.cpp
index e96899e25..0918112fc 100644
--- a/src/contrib/websocket_remote/WebSocketServer.cpp
+++ b/src/contrib/websocket_remote/WebSocketServer.cpp
@@ -727,6 +727,7 @@ void WebSocketServer::BroadcastPlayQueueChanged() {
 json WebSocketServer::WebSocketServer::ReadTrackMetadata(IRetainedTrack* track) {
     return {
         { key::id, track->GetId() },
+        { key::external_id, GetMetadataString(track, key::external_id) },
         { key::title, GetMetadataString(track, key::title) },
         { key::album, GetMetadataString(track, key::album) },
         { key::album_id, track->GetInt64(key::album_id.c_str()) },
diff --git a/src/core/library/Indexer.cpp b/src/core/library/Indexer.cpp
index 37350a250..39d761f7c 100644
--- a/src/core/library/Indexer.cpp
+++ b/src/core/library/Indexer.cpp
@@ -65,7 +65,6 @@
 static const std::string TAG = "Indexer";
 static const int MAX_THREADS = 2;
 static const size_t TRANSACTION_INTERVAL = 300;
-static std::atomic<int64_t> nextExternalId;
 
 using namespace musik::core;
 using namespace musik::core::sdk;
@@ -246,16 +245,6 @@ void Indexer::Synchronize(const SyncContext& context, boost::asio::io_service* i
 
     /* process local files */
     if (type == SyncType::All || type == SyncType::Local) {
-        /* resolve our next external id. we do this once before starting so
-        we don't need to make a bunch of additional queries while indexing. */
-        {
-            db::Statement stmt("SELECT MAX(id) FROM tracks", this->dbConnection);
-            if (stmt.Step() == db::Row) {
-                auto id = std::max((int64_t) 1, stmt.ColumnInt64(0));
-                nextExternalId.store(id);
-            }
-        }
-
         std::vector<std::string> paths;
         std::vector<int64_t> pathIds;
 
@@ -376,9 +365,6 @@ void Indexer::ReadMetadataFromFile(
 
         /* write it to the db, if read successfully */
         if (saveToDb) {
-            std::string externalId = "local://" + std::to_string(nextExternalId.fetch_add(1));
-            track.SetValue("external_id", externalId.c_str());
-
             track.SetValue("path_id", pathId.c_str());
             track.Save(this->dbConnection, this->libraryPath);
 
diff --git a/src/core/library/LocalLibrary.cpp b/src/core/library/LocalLibrary.cpp
index 0bd3b3e4f..557c2aeee 100644
--- a/src/core/library/LocalLibrary.cpp
+++ b/src/core/library/LocalLibrary.cpp
@@ -50,7 +50,7 @@ using namespace musik::core;
 using namespace musik::core::library;
 using namespace musik::core::runtime;
 
-#define DATABASE_VERSION 3
+#define DATABASE_VERSION 4
 #define VERBOSE_LOGGING 0
 #define MESSAGE_QUERY_COMPLETED 5000
 
@@ -299,6 +299,11 @@ static void upgradeV2ToV3(db::Connection& db) {
     scheduleSyncDueToDbUpgrade = true;
 }
 
+static void upgradeV3ToV4(db::Connection& db) {
+    db.Execute("DELETE from tracks");
+    scheduleSyncDueToDbUpgrade = true;
+}
+
 static void setVersion(db::Connection& db, int version) {
     db.Execute("DELETE FROM version");
 
@@ -481,6 +486,10 @@ void LocalLibrary::CreateDatabase(db::Connection &db){
         upgradeV2ToV3(db);
     }
 
+    if (lastVersion >= 1 && lastVersion < 4) {
+        upgradeV3ToV4(db);
+    }
+
     /* ensure our version is set correctly */
     setVersion(db, DATABASE_VERSION);
 
diff --git a/src/core/library/track/IndexerTrack.cpp b/src/core/library/track/IndexerTrack.cpp
index d4344850d..31d9888c8 100644
--- a/src/core/library/track/IndexerTrack.cpp
+++ b/src/core/library/track/IndexerTrack.cpp
@@ -43,7 +43,9 @@
 #include <core/io/DataStreamFactory.h>
 
 #include <boost/lexical_cast.hpp>
-
+#include <boost/uuid/uuid.hpp>
+#include <boost/uuid/uuid_generators.hpp>
+#include <boost/uuid/uuid_io.hpp>
 #include <unordered_map>
 
 using namespace musik::core;
@@ -60,6 +62,7 @@ using namespace musik::core;
 
 static std::mutex trackWriteLock;
 static std::unordered_map<std::string, int64_t> metadataIdCache;
+static auto uuids = boost::uuids::random_generator();
 
 void IndexerTrack::ResetIdCache() {
     metadataIdCache.clear();
@@ -615,6 +618,10 @@ bool IndexerTrack::Save(db::Connection &dbConnection, std::string libraryDirecto
         this->SetValue("album_artist", this->GetValue("artist").c_str());
     }
 
+    if (this->GetValue("external_id") == "") {
+        this->SetValue("external_id", boost::uuids::to_string(uuids()).c_str());
+    }
+
     /* remove existing relations -- we're going to update them with fresh data */
 
     if (this->id != 0) {
diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java
index 3b7107845..ec1ec4a9f 100644
--- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java
+++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java
@@ -28,6 +28,8 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
 import com.google.android.exoplayer2.util.Util;
 
 import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
 
 import io.casey.musikcube.remote.Application;
 import io.casey.musikcube.remote.util.NetworkUtil;
@@ -35,8 +37,21 @@ import okhttp3.Cache;
 import okhttp3.OkHttpClient;
 
 public class ExoPlayerWrapper extends PlayerWrapper {
-    private static OkHttpClient audioStreamClient = null;
-    private static boolean certValidationDisabled = false;
+    private static OkHttpClient audioStreamHttpClient = null;
+
+    private static final long BYTES_PER_MEGABYTE = 1048576L;
+    private static final long BYTES_PER_GIGABYTE = 1073741824L;
+    private static final Map<Integer, Long> CACHE_SETTING_TO_BYTES;
+
+    static {
+        CACHE_SETTING_TO_BYTES = new HashMap<>();
+        CACHE_SETTING_TO_BYTES.put(0, BYTES_PER_MEGABYTE * 32);
+        CACHE_SETTING_TO_BYTES.put(1, BYTES_PER_GIGABYTE / 2);
+        CACHE_SETTING_TO_BYTES.put(2, BYTES_PER_GIGABYTE);
+        CACHE_SETTING_TO_BYTES.put(3, BYTES_PER_GIGABYTE * 2);
+        CACHE_SETTING_TO_BYTES.put(4, BYTES_PER_GIGABYTE * 3);
+        CACHE_SETTING_TO_BYTES.put(5, BYTES_PER_GIGABYTE * 4);
+    }
 
     private DefaultBandwidthMeter bandwidth;
     private DataSource.Factory datasources;
@@ -47,6 +62,12 @@ public class ExoPlayerWrapper extends PlayerWrapper {
     private Context context;
     private SharedPreferences prefs;
 
+    public static void invalidateSettings() {
+        synchronized (ExoPlayerWrapper.class) {
+            audioStreamHttpClient = null;
+        }
+    }
+
     public ExoPlayerWrapper() {
         this.context = Application.getInstance();
         this.prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE);
@@ -56,37 +77,34 @@ public class ExoPlayerWrapper extends PlayerWrapper {
         this.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector);
         this.extractors = new DefaultExtractorsFactory();
         this.player.addListener(eventListener);
-
-        synchronized (ExoPlayerWrapper.class) {
-            final boolean disabled = this.prefs.getBoolean("cert_validation_disabled", false);
-            if (disabled != certValidationDisabled) {
-                audioStreamClient = null;
-                certValidationDisabled = disabled;
-            }
-        }
     }
 
-    private void initDataSourceFactory(final String uri) {
+    private void initHttpClient(final String uri) {
         final Context context = Application.getInstance();
 
         synchronized (ExoPlayerWrapper.class) {
-            if (audioStreamClient == null) {
+            if (audioStreamHttpClient == null) {
                 final File path = new File(context.getExternalCacheDir(), "audio");
 
-                OkHttpClient.Builder builder = new OkHttpClient.Builder()
-                    .cache(new Cache(path, 1048576 * 256)); /* 256 meg cache */
+                int diskCacheIndex = this.prefs.getInt("disk_cache_size_index", 0);
+                if (diskCacheIndex < 0 || diskCacheIndex > CACHE_SETTING_TO_BYTES.size()) {
+                    diskCacheIndex = 0;
+                }
 
-                if (certValidationDisabled) {
+                OkHttpClient.Builder builder = new OkHttpClient.Builder()
+                    .cache(new Cache(path, CACHE_SETTING_TO_BYTES.get(diskCacheIndex)));
+
+                if (this.prefs.getBoolean("cert_validation_disabled", false)) {
                     NetworkUtil.disableCertificateValidation(builder);
                 }
 
-                audioStreamClient = builder.build();
+                audioStreamHttpClient = builder.build();
             }
         }
 
         if (uri.startsWith("http")) {
             this.datasources = new OkHttpDataSourceFactory(
-                audioStreamClient,
+                audioStreamHttpClient,
                 Util.getUserAgent(context, "musikdroid"),
                 bandwidth);
         }
@@ -98,7 +116,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
 
     @Override
     public void play(String uri) {
-        initDataSourceFactory(uri);
+        initHttpClient(uri);
         this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
         this.player.setPlayWhenReady(true);
         this.player.prepare(this.source);
@@ -108,7 +126,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
 
     @Override
     public void prefetch(String uri) {
-        initDataSourceFactory(uri);
+        initHttpClient(uri);
         this.prefetch = true;
         this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
         this.player.setPlayWhenReady(false);
diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java
index 2aed9d332..8b5263ede 100644
--- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java
+++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java
@@ -13,6 +13,7 @@ import android.util.Log;
 import org.json.JSONArray;
 import org.json.JSONObject;
 
+import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -535,8 +536,8 @@ public class StreamingPlaybackService implements PlaybackService {
 
     private String getUri(final JSONObject track) {
         if (track != null) {
-            final long trackId = track.optLong("id", -1);
-            if (trackId != -1) {
+            final String externalId = track.optString("external_id", "");
+            if (Strings.notEmpty(externalId)) {
                 final String protocol = prefs.getBoolean("ssl_enabled", false) ? "https" : "http";
 
                 /* transcoding bitrate, if selected by the user */
@@ -553,11 +554,11 @@ public class StreamingPlaybackService implements PlaybackService {
 
                 return String.format(
                     Locale.ENGLISH,
-                    "%s://%s:%d/audio/id/%d%s",
+                    "%s://%s:%d/audio/external_id/%s%s",
                     protocol,
                     prefs.getString("address", "192.168.1.100"),
                     prefs.getInt("http_port", 7906),
-                    trackId,
+                    URLEncoder.encode(externalId),
                     bitrateQueryParam);
             }
         }
diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java
index 07de55703..98dc52ded 100644
--- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java
+++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java
@@ -10,8 +10,10 @@ import android.support.annotation.Nullable;
 import android.support.v4.app.DialogFragment;
 import android.support.v7.app.AlertDialog;
 import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
+import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.CheckBox;
 import android.widget.EditText;
@@ -20,6 +22,7 @@ import android.widget.Spinner;
 import java.util.Locale;
 
 import io.casey.musikcube.remote.R;
+import io.casey.musikcube.remote.playback.ExoPlayerWrapper;
 import io.casey.musikcube.remote.playback.MediaPlayerWrapper;
 import io.casey.musikcube.remote.playback.PlaybackServiceFactory;
 import io.casey.musikcube.remote.ui.util.Views;
@@ -29,8 +32,9 @@ public class SettingsActivity extends AppCompatActivity {
     private EditText addressText, portText, httpPortText, passwordText;
     private CheckBox albumArtCheckbox, messageCompressionCheckbox, softwareVolume;
     private CheckBox sslCheckbox, certCheckbox;
-    private Spinner playbackModeSpinner, bitrateSpinner;
+    private Spinner playbackModeSpinner, bitrateSpinner, cacheSpinner;
     private SharedPreferences prefs;
+    private boolean wasStreaming;
 
     public static Intent getStartIntent(final Context context) {
         return new Intent(context, SettingsActivity.class);
@@ -42,16 +46,27 @@ public class SettingsActivity extends AppCompatActivity {
         prefs = this.getSharedPreferences("prefs", MODE_PRIVATE);
         setContentView(R.layout.activity_settings);
         setTitle(R.string.settings_title);
+        wasStreaming = isStreamingEnabled();
         bindEventListeners();
         rebindUi();
     }
 
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.settings_menu, menu);
+        return true;
+    }
+
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == android.R.id.home) {
             finish();
             return true;
         }
+        else if (item.getItemId() == R.id.action_save) {
+            save();
+            return true;
+        }
 
         return super.onOptionsItemSelected(item);
     }
@@ -76,6 +91,13 @@ public class SettingsActivity extends AppCompatActivity {
         bitrateSpinner.setAdapter(bitrates);
         bitrateSpinner.setSelection(prefs.getInt("transcoder_bitrate_index", 0));
 
+        final ArrayAdapter<CharSequence> cacheSizes = ArrayAdapter.createFromResource(
+            this, R.array.disk_cache_array, android.R.layout.simple_spinner_item);
+
+        cacheSizes.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        cacheSpinner.setAdapter(cacheSizes);
+        cacheSpinner.setSelection(prefs.getInt("disk_cache_size_index", 0));
+
         this.albumArtCheckbox.setChecked(this.prefs.getBoolean("album_art_enabled", true));
         this.messageCompressionCheckbox.setChecked(this.prefs.getBoolean("message_compression_enabled", true));
         this.softwareVolume.setChecked(this.prefs.getBoolean("software_volume", false));
@@ -137,43 +159,58 @@ public class SettingsActivity extends AppCompatActivity {
         this.softwareVolume = (CheckBox) findViewById(R.id.software_volume);
         this.playbackModeSpinner = (Spinner) findViewById(R.id.playback_mode_spinner);
         this.bitrateSpinner = (Spinner) findViewById(R.id.transcoder_bitrate_spinner);
+        this.cacheSpinner = (Spinner) findViewById(R.id.streaming_disk_cache_spinner);
         this.sslCheckbox = (CheckBox) findViewById(R.id.ssl_checkbox);
         this.certCheckbox = (CheckBox) findViewById(R.id.cert_validation);
 
-        final boolean wasStreaming = isStreamingEnabled();
-
-        this.findViewById(R.id.button_connect).setOnClickListener((View v) -> {
-            final String addr = addressText.getText().toString();
-            final String port = portText.getText().toString();
-            final String httpPort = httpPortText.getText().toString();
-            final String password = passwordText.getText().toString();
-
-            prefs.edit()
-                .putString("address", addr)
-                .putInt("port", (port.length() > 0) ? Integer.valueOf(port) : 0)
-                .putInt("http_port", (httpPort.length() > 0) ? Integer.valueOf(httpPort) : 0)
-                .putString("password", password)
-                .putBoolean("album_art_enabled", albumArtCheckbox.isChecked())
-                .putBoolean("message_compression_enabled", messageCompressionCheckbox.isChecked())
-                .putBoolean("streaming_playback", isStreamingSelected())
-                .putBoolean("software_volume", softwareVolume.isChecked())
-                .putBoolean("ssl_enabled", sslCheckbox.isChecked())
-                .putBoolean("cert_validation_disabled", certCheckbox.isChecked())
-                .putInt("transcoder_bitrate_index", bitrateSpinner.getSelectedItemPosition())
-                .apply();
-
-            if (!softwareVolume.isChecked()) {
-                MediaPlayerWrapper.setGlobalVolume(1.0f);
+        this.playbackModeSpinner.setOnItemSelectedListener(new Spinner.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> adapterView, View view, int selectedIndex, long l) {
+                final boolean streaming = (selectedIndex == 1);
+                bitrateSpinner.setEnabled(streaming);
+                cacheSpinner.setEnabled(streaming);
             }
 
-            if (wasStreaming && !isStreamingEnabled()) {
-                PlaybackServiceFactory.streaming(this).stop();
+            @Override
+            public void onNothingSelected(AdapterView<?> adapterView) {
+
             }
-
-            WebSocketService.getInstance(this).disconnect();
-
-            finish();
         });
+   }
+
+    private void save() {
+        final String addr = addressText.getText().toString();
+        final String port = portText.getText().toString();
+        final String httpPort = httpPortText.getText().toString();
+        final String password = passwordText.getText().toString();
+
+        prefs.edit()
+            .putString("address", addr)
+            .putInt("port", (port.length() > 0) ? Integer.valueOf(port) : 0)
+            .putInt("http_port", (httpPort.length() > 0) ? Integer.valueOf(httpPort) : 0)
+            .putString("password", password)
+            .putBoolean("album_art_enabled", albumArtCheckbox.isChecked())
+            .putBoolean("message_compression_enabled", messageCompressionCheckbox.isChecked())
+            .putBoolean("streaming_playback", isStreamingSelected())
+            .putBoolean("software_volume", softwareVolume.isChecked())
+            .putBoolean("ssl_enabled", sslCheckbox.isChecked())
+            .putBoolean("cert_validation_disabled", certCheckbox.isChecked())
+            .putInt("transcoder_bitrate_index", bitrateSpinner.getSelectedItemPosition())
+            .putInt("disk_cache_size_index", cacheSpinner.getSelectedItemPosition())
+            .apply();
+
+        if (!softwareVolume.isChecked()) {
+            MediaPlayerWrapper.setGlobalVolume(1.0f);
+        }
+
+        if (wasStreaming && !isStreamingEnabled()) {
+            PlaybackServiceFactory.streaming(this).stop();
+        }
+
+        ExoPlayerWrapper.invalidateSettings();
+        WebSocketService.getInstance(this).disconnect();
+
+        finish();
     }
 
     public static class SslAlertDialog extends DialogFragment {
diff --git a/src/musikdroid/app/src/main/res/layout/activity_settings.xml b/src/musikdroid/app/src/main/res/layout/activity_settings.xml
index e4b372ec4..1efa15c5a 100644
--- a/src/musikdroid/app/src/main/res/layout/activity_settings.xml
+++ b/src/musikdroid/app/src/main/res/layout/activity_settings.xml
@@ -1,192 +1,193 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
+<ScrollView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
+    android:layout_height="wrap_content"
+    android:fillViewport="true">
 
-    <ScrollView
+    <LinearLayout
+        android:orientation="vertical"
         android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1">
+        android:layout_height="wrap_content"
+        android:padding="16dp">
 
-        <LinearLayout
-            android:orientation="vertical"
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="4dp"
+            android:layout_marginBottom="16dp"
+            android:textColor="@color/theme_foreground"
+            android:text="@string/edit_connection_info"/>
+
+        <android.support.design.widget.TextInputLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:padding="16dp">
+            android:layout_marginBottom="8dp"
+            android:layout_marginLeft="24dp">
 
-            <TextView
-                android:layout_width="wrap_content"
+            <EditText
+                android:id="@+id/address"
+                android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="4dp"
-                android:layout_marginBottom="16dp"
-                android:textColor="@color/theme_foreground"
-                android:text="@string/edit_connection_info"/>
+                android:maxLines="1"
+                android:hint="@string/edit_connection_hostname"
+                android:inputType="textNoSuggestions" />
 
-            <android.support.design.widget.TextInputLayout
-                android:layout_width="match_parent"
+        </android.support.design.widget.TextInputLayout>
+
+        <android.support.design.widget.TextInputLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="12dp"
+            android:layout_marginLeft="24dp">
+
+            <EditText
+                android:id="@+id/port"
+                android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginBottom="8dp"
-                android:layout_marginLeft="24dp">
+                android:maxLines="1"
+                android:hint="@string/edit_connection_port"
+                android:inputType="number" />
 
-                <EditText
-                    android:id="@+id/address"
-                    android:layout_width="fill_parent"
-                    android:layout_height="wrap_content"
-                    android:maxLines="1"
-                    android:hint="@string/edit_connection_hostname"
-                    android:inputType="textNoSuggestions" />
+        </android.support.design.widget.TextInputLayout>
 
-            </android.support.design.widget.TextInputLayout>
+        <android.support.design.widget.TextInputLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="12dp"
+            android:layout_marginLeft="24dp">
 
-            <android.support.design.widget.TextInputLayout
-                android:layout_width="match_parent"
+            <EditText
+                android:id="@+id/http_port"
+                android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginBottom="12dp"
-                android:layout_marginLeft="24dp">
+                android:maxLines="1"
+                android:hint="@string/edit_connection_http_port"
+                android:inputType="number" />
 
-                <EditText
-                    android:id="@+id/port"
-                    android:layout_width="fill_parent"
-                    android:layout_height="wrap_content"
-                    android:maxLines="1"
-                    android:hint="@string/edit_connection_port"
-                    android:inputType="number" />
+        </android.support.design.widget.TextInputLayout>
 
-            </android.support.design.widget.TextInputLayout>
+        <android.support.design.widget.TextInputLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="12dp"
+            android:layout_marginLeft="24dp">
 
-            <android.support.design.widget.TextInputLayout
-                android:layout_width="match_parent"
+            <EditText
+                android:id="@+id/password"
+                android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginBottom="12dp"
-                android:layout_marginLeft="24dp">
+                android:maxLines="1"
+                android:hint="@string/edit_connection_password"
+                android:inputType="textPassword" />
 
-                <EditText
-                    android:id="@+id/http_port"
-                    android:layout_width="fill_parent"
-                    android:layout_height="wrap_content"
-                    android:maxLines="1"
-                    android:hint="@string/edit_connection_http_port"
-                    android:inputType="number" />
+        </android.support.design.widget.TextInputLayout>
 
-            </android.support.design.widget.TextInputLayout>
+        <android.support.v4.widget.Space
+            android:layout_width="0dp"
+            android:layout_height="8dp"/>
 
-            <android.support.design.widget.TextInputLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginBottom="12dp"
-                android:layout_marginLeft="24dp">
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginRight="8dp"
+            android:text="@string/settings_playback_mode"/>
 
-                <EditText
-                    android:id="@+id/password"
-                    android:layout_width="fill_parent"
-                    android:layout_height="wrap_content"
-                    android:maxLines="1"
-                    android:hint="@string/edit_connection_password"
-                    android:inputType="textPassword" />
+        <Spinner
+            android:id="@+id/playback_mode_spinner"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_marginLeft="24dp"/>
 
-            </android.support.design.widget.TextInputLayout>
+        <android.support.v4.widget.Space
+            android:layout_width="0dp"
+            android:layout_height="16dp"/>
 
-            <android.support.v4.widget.Space
-                android:layout_width="0dp"
-                android:layout_height="8dp"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginRight="8dp"
+            android:text="@string/settings_transcoder_bitrate"/>
 
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_vertical"
-                android:layout_marginRight="8dp"
-                android:text="@string/settings_playback_mode"/>
+        <Spinner
+            android:id="@+id/transcoder_bitrate_spinner"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_marginLeft="24dp"/>
 
-            <Spinner
-                android:id="@+id/playback_mode_spinner"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                android:layout_marginLeft="24dp"/>
+        <android.support.v4.widget.Space
+            android:layout_width="0dp"
+            android:layout_height="16dp"/>
 
-            <android.support.v4.widget.Space
-                android:layout_width="0dp"
-                android:layout_height="16dp"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginRight="8dp"
+            android:text="@string/settings_cache_size"/>
 
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_vertical"
-                android:layout_marginRight="8dp"
-                android:text="@string/settings_transcoder_bitrate"/>
+        <Spinner
+            android:id="@+id/streaming_disk_cache_spinner"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_marginLeft="24dp"/>
 
-            <Spinner
-                android:id="@+id/transcoder_bitrate_spinner"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                android:layout_marginLeft="24dp"/>
+        <android.support.v4.widget.Space
+            android:layout_width="0dp"
+            android:layout_height="16dp"/>
 
-            <android.support.v4.widget.Space
-                android:layout_width="0dp"
-                android:layout_height="16dp"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginRight="8dp"
+            android:layout_marginBottom="8dp"
+            android:text="@string/settings_advanced"/>
 
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_vertical"
-                android:layout_marginRight="8dp"
-                android:layout_marginBottom="8dp"
-                android:text="@string/settings_general"/>
+        <CheckBox
+            android:id="@+id/album_art_checkbox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@color/theme_foreground"
+            android:layout_marginLeft="24dp"
+            android:text="@string/settings_enable_album_art"/>
 
-            <CheckBox
-                android:id="@+id/album_art_checkbox"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:textColor="@color/theme_foreground"
-                android:layout_marginLeft="24dp"
-                android:text="@string/settings_enable_album_art"/>
+        <CheckBox
+            android:id="@+id/ssl_checkbox"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/theme_foreground"
+            android:layout_marginLeft="24dp"
+            android:text="@string/settings_use_ssl"/>
 
-            <CheckBox
-                android:id="@+id/ssl_checkbox"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:textColor="@color/theme_foreground"
-                android:layout_marginLeft="24dp"
-                android:text="@string/settings_use_ssl"/>
+        <CheckBox
+            android:id="@+id/cert_validation"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/theme_foreground"
+            android:layout_marginLeft="24dp"
+            android:visibility="visible"
+            android:text="@string/settings_disable_cert_validation"/>
 
-            <CheckBox
-                android:id="@+id/cert_validation"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:textColor="@color/theme_foreground"
-                android:layout_marginLeft="24dp"
-                android:visibility="visible"
-                android:text="@string/settings_disable_cert_validation"/>
+        <CheckBox
+            android:id="@+id/message_compression"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@color/theme_foreground"
+            android:layout_marginLeft="24dp"
+            android:visibility="gone"
+            android:text="@string/settings_enable_message_compression"/>
 
-            <CheckBox
-                android:id="@+id/message_compression"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:textColor="@color/theme_foreground"
-                android:layout_marginLeft="24dp"
-                android:visibility="gone"
-                android:text="@string/settings_enable_message_compression"/>
+        <CheckBox
+            android:id="@+id/software_volume"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@color/theme_foreground"
+            android:layout_marginLeft="24dp"
+            android:text="@string/settings_enable_software_volume"/>
 
-            <CheckBox
-                android:id="@+id/software_volume"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:textColor="@color/theme_foreground"
-                android:layout_marginLeft="24dp"
-                android:text="@string/settings_enable_software_volume"/>
+    </LinearLayout>
 
-        </LinearLayout>
-
-    </ScrollView>
-
-    <TextView
-        style="@style/BrowseButton"
-        android:id="@+id/button_connect"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_margin="12dp"
-        android:layout_gravity="right"
-        android:text="@string/button_save"/>
-
-</LinearLayout>
\ No newline at end of file
+</ScrollView>
\ No newline at end of file
diff --git a/src/musikdroid/app/src/main/res/menu/settings_menu.xml b/src/musikdroid/app/src/main/res/menu/settings_menu.xml
new file mode 100644
index 000000000..c3f527672
--- /dev/null
+++ b/src/musikdroid/app/src/main/res/menu/settings_menu.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_save"
+        app:showAsAction="always"
+        android:title="@string/button_save"/>
+
+</menu>
\ No newline at end of file
diff --git a/src/musikdroid/app/src/main/res/values/disk_cache.xml b/src/musikdroid/app/src/main/res/values/disk_cache.xml
new file mode 100644
index 000000000..2a8aa9c22
--- /dev/null
+++ b/src/musikdroid/app/src/main/res/values/disk_cache.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string-array name="disk_cache_array">
+        <item>disabled</item>
+        <item>0.5 gb</item>
+        <item>1 gb</item>
+        <item>2 gb</item>
+        <item>3 gb</item>
+        <item>4 gb</item>
+    </string-array>
+</resources>
\ No newline at end of file
diff --git a/src/musikdroid/app/src/main/res/values/strings.xml b/src/musikdroid/app/src/main/res/values/strings.xml
index 9d5e5ea23..61220b2f2 100644
--- a/src/musikdroid/app/src/main/res/values/strings.xml
+++ b/src/musikdroid/app/src/main/res/values/strings.xml
@@ -53,8 +53,9 @@
     <string name="menu_playlists">playlists</string>
     <string name="unknown_value">&lt;unknown&gt;</string>
     <string name="settings_playback_mode">playback mode:</string>
-    <string name="settings_transcoder_bitrate">transcoder bitrate:</string>
-    <string name="settings_general">general:</string>
+    <string name="settings_transcoder_bitrate">streaming downsampler bitrate:</string>
+    <string name="settings_cache_size">streaming disk cache size:</string>
+    <string name="settings_advanced">advanced:</string>
     <string name="settings_use_ssl">use ssl for server connections</string>
     <string name="settings_enable_album_art">enable album art (uses last.fm)</string>
     <string name="settings_enable_message_compression">enable message compression</string>