- 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
This commit is contained in:
casey langen 2017-05-17 23:15:59 -07:00
parent 646742214d
commit f8e201cb80
13 changed files with 322 additions and 230 deletions

View File

@ -76,6 +76,7 @@ namespace key {
static const std::string playing_current_time = "playing_current_time"; static const std::string playing_current_time = "playing_current_time";
static const std::string playing_track = "playing_track"; static const std::string playing_track = "playing_track";
static const std::string title = "title"; static const std::string title = "title";
static const std::string external_id = "external_id";
static const std::string filename = "filename"; static const std::string filename = "filename";
static const std::string artist = "artist"; static const std::string artist = "artist";
static const std::string album = "album"; static const std::string album = "album";

View File

@ -288,15 +288,16 @@ int HttpServer::HandleRequest(
if (parts.size() > 0) { if (parts.size() > 0) {
if (parts.at(0) == fragment::audio && parts.size() == 3) { if (parts.at(0) == fragment::audio && parts.size() == 3) {
IRetainedTrack* track = nullptr; IRetainedTrack* track = nullptr;
bool byExternalId = (parts.at(1) == fragment::external_id);
if (parts.at(1) == fragment::id) { if (byExternalId) {
uint64_t id = std::stoull(urlDecode(parts.at(2)));
track = server->context.dataProvider->QueryTrackById(id);
}
else if (parts.at(1) == fragment::external_id) {
std::string externalId = urlDecode(parts.at(2)); std::string externalId = urlDecode(parts.at(2));
track = server->context.dataProvider->QueryTrackByExternalId(externalId.c_str()); 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) { if (track) {
std::string filename = GetMetadataString(track, key::filename); std::string filename = GetMetadataString(track, key::filename);
@ -360,6 +361,13 @@ int HttpServer::HandleRequest(
MHD_add_response_header(response, "Accept-Ranges", "bytes"); 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, "Content-Type", contentType(filename).c_str());
MHD_add_response_header(response, "Server", "musikcube websocket_remote"); MHD_add_response_header(response, "Server", "musikcube websocket_remote");

View File

@ -727,6 +727,7 @@ void WebSocketServer::BroadcastPlayQueueChanged() {
json WebSocketServer::WebSocketServer::ReadTrackMetadata(IRetainedTrack* track) { json WebSocketServer::WebSocketServer::ReadTrackMetadata(IRetainedTrack* track) {
return { return {
{ key::id, track->GetId() }, { key::id, track->GetId() },
{ key::external_id, GetMetadataString(track, key::external_id) },
{ key::title, GetMetadataString(track, key::title) }, { key::title, GetMetadataString(track, key::title) },
{ key::album, GetMetadataString(track, key::album) }, { key::album, GetMetadataString(track, key::album) },
{ key::album_id, track->GetInt64(key::album_id.c_str()) }, { key::album_id, track->GetInt64(key::album_id.c_str()) },

View File

@ -65,7 +65,6 @@
static const std::string TAG = "Indexer"; static const std::string TAG = "Indexer";
static const int MAX_THREADS = 2; static const int MAX_THREADS = 2;
static const size_t TRANSACTION_INTERVAL = 300; static const size_t TRANSACTION_INTERVAL = 300;
static std::atomic<int64_t> nextExternalId;
using namespace musik::core; using namespace musik::core;
using namespace musik::core::sdk; using namespace musik::core::sdk;
@ -246,16 +245,6 @@ void Indexer::Synchronize(const SyncContext& context, boost::asio::io_service* i
/* process local files */ /* process local files */
if (type == SyncType::All || type == SyncType::Local) { 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<std::string> paths;
std::vector<int64_t> pathIds; std::vector<int64_t> pathIds;
@ -376,9 +365,6 @@ void Indexer::ReadMetadataFromFile(
/* write it to the db, if read successfully */ /* write it to the db, if read successfully */
if (saveToDb) { 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.SetValue("path_id", pathId.c_str());
track.Save(this->dbConnection, this->libraryPath); track.Save(this->dbConnection, this->libraryPath);

View File

@ -50,7 +50,7 @@ using namespace musik::core;
using namespace musik::core::library; using namespace musik::core::library;
using namespace musik::core::runtime; using namespace musik::core::runtime;
#define DATABASE_VERSION 3 #define DATABASE_VERSION 4
#define VERBOSE_LOGGING 0 #define VERBOSE_LOGGING 0
#define MESSAGE_QUERY_COMPLETED 5000 #define MESSAGE_QUERY_COMPLETED 5000
@ -299,6 +299,11 @@ static void upgradeV2ToV3(db::Connection& db) {
scheduleSyncDueToDbUpgrade = true; scheduleSyncDueToDbUpgrade = true;
} }
static void upgradeV3ToV4(db::Connection& db) {
db.Execute("DELETE from tracks");
scheduleSyncDueToDbUpgrade = true;
}
static void setVersion(db::Connection& db, int version) { static void setVersion(db::Connection& db, int version) {
db.Execute("DELETE FROM version"); db.Execute("DELETE FROM version");
@ -481,6 +486,10 @@ void LocalLibrary::CreateDatabase(db::Connection &db){
upgradeV2ToV3(db); upgradeV2ToV3(db);
} }
if (lastVersion >= 1 && lastVersion < 4) {
upgradeV3ToV4(db);
}
/* ensure our version is set correctly */ /* ensure our version is set correctly */
setVersion(db, DATABASE_VERSION); setVersion(db, DATABASE_VERSION);

View File

@ -43,7 +43,9 @@
#include <core/io/DataStreamFactory.h> #include <core/io/DataStreamFactory.h>
#include <boost/lexical_cast.hpp> #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> #include <unordered_map>
using namespace musik::core; using namespace musik::core;
@ -60,6 +62,7 @@ using namespace musik::core;
static std::mutex trackWriteLock; static std::mutex trackWriteLock;
static std::unordered_map<std::string, int64_t> metadataIdCache; static std::unordered_map<std::string, int64_t> metadataIdCache;
static auto uuids = boost::uuids::random_generator();
void IndexerTrack::ResetIdCache() { void IndexerTrack::ResetIdCache() {
metadataIdCache.clear(); metadataIdCache.clear();
@ -615,6 +618,10 @@ bool IndexerTrack::Save(db::Connection &dbConnection, std::string libraryDirecto
this->SetValue("album_artist", this->GetValue("artist").c_str()); 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 */ /* remove existing relations -- we're going to update them with fresh data */
if (this->id != 0) { if (this->id != 0) {

View File

@ -28,6 +28,8 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.util.HashMap;
import java.util.Map;
import io.casey.musikcube.remote.Application; import io.casey.musikcube.remote.Application;
import io.casey.musikcube.remote.util.NetworkUtil; import io.casey.musikcube.remote.util.NetworkUtil;
@ -35,8 +37,21 @@ import okhttp3.Cache;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
public class ExoPlayerWrapper extends PlayerWrapper { public class ExoPlayerWrapper extends PlayerWrapper {
private static OkHttpClient audioStreamClient = null; private static OkHttpClient audioStreamHttpClient = null;
private static boolean certValidationDisabled = false;
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 DefaultBandwidthMeter bandwidth;
private DataSource.Factory datasources; private DataSource.Factory datasources;
@ -47,6 +62,12 @@ public class ExoPlayerWrapper extends PlayerWrapper {
private Context context; private Context context;
private SharedPreferences prefs; private SharedPreferences prefs;
public static void invalidateSettings() {
synchronized (ExoPlayerWrapper.class) {
audioStreamHttpClient = null;
}
}
public ExoPlayerWrapper() { public ExoPlayerWrapper() {
this.context = Application.getInstance(); this.context = Application.getInstance();
this.prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE); 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.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector);
this.extractors = new DefaultExtractorsFactory(); this.extractors = new DefaultExtractorsFactory();
this.player.addListener(eventListener); 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(); final Context context = Application.getInstance();
synchronized (ExoPlayerWrapper.class) { synchronized (ExoPlayerWrapper.class) {
if (audioStreamClient == null) { if (audioStreamHttpClient == null) {
final File path = new File(context.getExternalCacheDir(), "audio"); final File path = new File(context.getExternalCacheDir(), "audio");
OkHttpClient.Builder builder = new OkHttpClient.Builder() int diskCacheIndex = this.prefs.getInt("disk_cache_size_index", 0);
.cache(new Cache(path, 1048576 * 256)); /* 256 meg cache */ 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); NetworkUtil.disableCertificateValidation(builder);
} }
audioStreamClient = builder.build(); audioStreamHttpClient = builder.build();
} }
} }
if (uri.startsWith("http")) { if (uri.startsWith("http")) {
this.datasources = new OkHttpDataSourceFactory( this.datasources = new OkHttpDataSourceFactory(
audioStreamClient, audioStreamHttpClient,
Util.getUserAgent(context, "musikdroid"), Util.getUserAgent(context, "musikdroid"),
bandwidth); bandwidth);
} }
@ -98,7 +116,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@Override @Override
public void play(String uri) { public void play(String uri) {
initDataSourceFactory(uri); initHttpClient(uri);
this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null); this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
this.player.setPlayWhenReady(true); this.player.setPlayWhenReady(true);
this.player.prepare(this.source); this.player.prepare(this.source);
@ -108,7 +126,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@Override @Override
public void prefetch(String uri) { public void prefetch(String uri) {
initDataSourceFactory(uri); initHttpClient(uri);
this.prefetch = true; this.prefetch = true;
this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null); this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
this.player.setPlayWhenReady(false); this.player.setPlayWhenReady(false);

View File

@ -13,6 +13,7 @@ import android.util.Log;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -535,8 +536,8 @@ public class StreamingPlaybackService implements PlaybackService {
private String getUri(final JSONObject track) { private String getUri(final JSONObject track) {
if (track != null) { if (track != null) {
final long trackId = track.optLong("id", -1); final String externalId = track.optString("external_id", "");
if (trackId != -1) { if (Strings.notEmpty(externalId)) {
final String protocol = prefs.getBoolean("ssl_enabled", false) ? "https" : "http"; final String protocol = prefs.getBoolean("ssl_enabled", false) ? "https" : "http";
/* transcoding bitrate, if selected by the user */ /* transcoding bitrate, if selected by the user */
@ -553,11 +554,11 @@ public class StreamingPlaybackService implements PlaybackService {
return String.format( return String.format(
Locale.ENGLISH, Locale.ENGLISH,
"%s://%s:%d/audio/id/%d%s", "%s://%s:%d/audio/external_id/%s%s",
protocol, protocol,
prefs.getString("address", "192.168.1.100"), prefs.getString("address", "192.168.1.100"),
prefs.getInt("http_port", 7906), prefs.getInt("http_port", 7906),
trackId, URLEncoder.encode(externalId),
bitrateQueryParam); bitrateQueryParam);
} }
} }

View File

@ -10,8 +10,10 @@ import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.EditText; import android.widget.EditText;
@ -20,6 +22,7 @@ import android.widget.Spinner;
import java.util.Locale; import java.util.Locale;
import io.casey.musikcube.remote.R; 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.MediaPlayerWrapper;
import io.casey.musikcube.remote.playback.PlaybackServiceFactory; import io.casey.musikcube.remote.playback.PlaybackServiceFactory;
import io.casey.musikcube.remote.ui.util.Views; import io.casey.musikcube.remote.ui.util.Views;
@ -29,8 +32,9 @@ public class SettingsActivity extends AppCompatActivity {
private EditText addressText, portText, httpPortText, passwordText; private EditText addressText, portText, httpPortText, passwordText;
private CheckBox albumArtCheckbox, messageCompressionCheckbox, softwareVolume; private CheckBox albumArtCheckbox, messageCompressionCheckbox, softwareVolume;
private CheckBox sslCheckbox, certCheckbox; private CheckBox sslCheckbox, certCheckbox;
private Spinner playbackModeSpinner, bitrateSpinner; private Spinner playbackModeSpinner, bitrateSpinner, cacheSpinner;
private SharedPreferences prefs; private SharedPreferences prefs;
private boolean wasStreaming;
public static Intent getStartIntent(final Context context) { public static Intent getStartIntent(final Context context) {
return new Intent(context, SettingsActivity.class); return new Intent(context, SettingsActivity.class);
@ -42,16 +46,27 @@ public class SettingsActivity extends AppCompatActivity {
prefs = this.getSharedPreferences("prefs", MODE_PRIVATE); prefs = this.getSharedPreferences("prefs", MODE_PRIVATE);
setContentView(R.layout.activity_settings); setContentView(R.layout.activity_settings);
setTitle(R.string.settings_title); setTitle(R.string.settings_title);
wasStreaming = isStreamingEnabled();
bindEventListeners(); bindEventListeners();
rebindUi(); rebindUi();
} }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.settings_menu, menu);
return true;
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) { if (item.getItemId() == android.R.id.home) {
finish(); finish();
return true; return true;
} }
else if (item.getItemId() == R.id.action_save) {
save();
return true;
}
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -76,6 +91,13 @@ public class SettingsActivity extends AppCompatActivity {
bitrateSpinner.setAdapter(bitrates); bitrateSpinner.setAdapter(bitrates);
bitrateSpinner.setSelection(prefs.getInt("transcoder_bitrate_index", 0)); 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.albumArtCheckbox.setChecked(this.prefs.getBoolean("album_art_enabled", true));
this.messageCompressionCheckbox.setChecked(this.prefs.getBoolean("message_compression_enabled", true)); this.messageCompressionCheckbox.setChecked(this.prefs.getBoolean("message_compression_enabled", true));
this.softwareVolume.setChecked(this.prefs.getBoolean("software_volume", false)); this.softwareVolume.setChecked(this.prefs.getBoolean("software_volume", false));
@ -137,12 +159,26 @@ public class SettingsActivity extends AppCompatActivity {
this.softwareVolume = (CheckBox) findViewById(R.id.software_volume); this.softwareVolume = (CheckBox) findViewById(R.id.software_volume);
this.playbackModeSpinner = (Spinner) findViewById(R.id.playback_mode_spinner); this.playbackModeSpinner = (Spinner) findViewById(R.id.playback_mode_spinner);
this.bitrateSpinner = (Spinner) findViewById(R.id.transcoder_bitrate_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.sslCheckbox = (CheckBox) findViewById(R.id.ssl_checkbox);
this.certCheckbox = (CheckBox) findViewById(R.id.cert_validation); this.certCheckbox = (CheckBox) findViewById(R.id.cert_validation);
final boolean wasStreaming = isStreamingEnabled(); 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);
}
this.findViewById(R.id.button_connect).setOnClickListener((View v) -> { @Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
}
private void save() {
final String addr = addressText.getText().toString(); final String addr = addressText.getText().toString();
final String port = portText.getText().toString(); final String port = portText.getText().toString();
final String httpPort = httpPortText.getText().toString(); final String httpPort = httpPortText.getText().toString();
@ -160,6 +196,7 @@ public class SettingsActivity extends AppCompatActivity {
.putBoolean("ssl_enabled", sslCheckbox.isChecked()) .putBoolean("ssl_enabled", sslCheckbox.isChecked())
.putBoolean("cert_validation_disabled", certCheckbox.isChecked()) .putBoolean("cert_validation_disabled", certCheckbox.isChecked())
.putInt("transcoder_bitrate_index", bitrateSpinner.getSelectedItemPosition()) .putInt("transcoder_bitrate_index", bitrateSpinner.getSelectedItemPosition())
.putInt("disk_cache_size_index", cacheSpinner.getSelectedItemPosition())
.apply(); .apply();
if (!softwareVolume.isChecked()) { if (!softwareVolume.isChecked()) {
@ -170,10 +207,10 @@ public class SettingsActivity extends AppCompatActivity {
PlaybackServiceFactory.streaming(this).stop(); PlaybackServiceFactory.streaming(this).stop();
} }
ExoPlayerWrapper.invalidateSettings();
WebSocketService.getInstance(this).disconnect(); WebSocketService.getInstance(this).disconnect();
finish(); finish();
});
} }
public static class SslAlertDialog extends DialogFragment { public static class SslAlertDialog extends DialogFragment {

View File

@ -1,14 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <ScrollView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical"> android:fillViewport="true">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="vertical"
@ -126,13 +121,30 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="16dp"/> 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"/>
<Spinner
android:id="@+id/streaming_disk_cache_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"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:text="@string/settings_general"/> android:text="@string/settings_advanced"/>
<CheckBox <CheckBox
android:id="@+id/album_art_checkbox" android:id="@+id/album_art_checkbox"
@ -179,14 +191,3 @@
</LinearLayout> </LinearLayout>
</ScrollView> </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>

View File

@ -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>

View File

@ -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>

View File

@ -53,8 +53,9 @@
<string name="menu_playlists">playlists</string> <string name="menu_playlists">playlists</string>
<string name="unknown_value">&lt;unknown&gt;</string> <string name="unknown_value">&lt;unknown&gt;</string>
<string name="settings_playback_mode">playback mode:</string> <string name="settings_playback_mode">playback mode:</string>
<string name="settings_transcoder_bitrate">transcoder bitrate:</string> <string name="settings_transcoder_bitrate">streaming downsampler bitrate:</string>
<string name="settings_general">general:</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_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_album_art">enable album art (uses last.fm)</string>
<string name="settings_enable_message_compression">enable message compression</string> <string name="settings_enable_message_compression">enable message compression</string>