- 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_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";

View File

@ -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");

View File

@ -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()) },

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

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

View File

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

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="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>