mirror of
https://github.com/clangen/musikcube.git
synced 2025-03-14 13:21:13 +00:00
Added support for simple password protected web socket sessions in the
server plugin and android client.
This commit is contained in:
parent
7905dc5e34
commit
f4510df6e3
@ -18,6 +18,7 @@
|
||||
#include <json.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
|
||||
#ifdef WIN32
|
||||
@ -27,6 +28,7 @@
|
||||
#endif
|
||||
|
||||
#define DEFAULT_PORT 9002
|
||||
#define DEFAULT_PASSWORD ""
|
||||
|
||||
using namespace musik::core::sdk;
|
||||
|
||||
@ -37,9 +39,10 @@ using websocketpp::lib::placeholders::_2;
|
||||
using websocketpp::lib::bind;
|
||||
using server = websocketpp::server<websocketpp::config::asio>;
|
||||
using connection_hdl = websocketpp::connection_hdl;
|
||||
using connection_list = std::set<connection_hdl, std::owner_less<connection_hdl>>;
|
||||
using message_ptr = server::message_ptr;
|
||||
|
||||
using ConnectionList = std::map<connection_hdl, bool, std::owner_less<connection_hdl>>;
|
||||
|
||||
typedef boost::shared_mutex Mutex;
|
||||
typedef boost::unique_lock<Mutex> WriteLock;
|
||||
typedef boost::shared_lock<Mutex> ReadLock;
|
||||
@ -105,10 +108,14 @@ namespace key {
|
||||
static const std::string index = "index";
|
||||
static const std::string delta = "delta";
|
||||
static const std::string relative = "relative";
|
||||
static const std::string password = "password";
|
||||
static const std::string port = "port";
|
||||
static const std::string authenticated = "authenticated";
|
||||
}
|
||||
|
||||
namespace value {
|
||||
static const std::string invalid = "invalid";
|
||||
static const std::string unauthenticated = "unauthenticated";
|
||||
}
|
||||
|
||||
namespace type {
|
||||
@ -118,6 +125,7 @@ namespace type {
|
||||
}
|
||||
|
||||
namespace request {
|
||||
static const std::string authenticate = "authenticate";
|
||||
static const std::string ping = "ping";
|
||||
static const std::string pause_or_resume = "pause_or_resume";
|
||||
static const std::string stop = "stop";
|
||||
@ -231,7 +239,39 @@ class PlaybackRemote : public IPlaybackRemote {
|
||||
}
|
||||
|
||||
private:
|
||||
void HandleAuthentication(connection_hdl connection, json& request) {
|
||||
std::string name = request[message::name];
|
||||
|
||||
if (name == request::authenticate) {
|
||||
std::string sent = request[message::options][key::password];
|
||||
|
||||
std::string actual = this->GetPreferenceString(
|
||||
::preferences, key::password, DEFAULT_PASSWORD);
|
||||
|
||||
if (sent == actual) {
|
||||
this->connections[connection] = true; /* mark as authed */
|
||||
|
||||
this->RespondWithOptions(
|
||||
connection,
|
||||
request,
|
||||
json({ { key::authenticated, true } }));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->wss.close(
|
||||
connection,
|
||||
websocketpp::close::status::policy_violation,
|
||||
value::unauthenticated);
|
||||
}
|
||||
|
||||
void HandleRequest(connection_hdl connection, json& request) {
|
||||
if (this->connections[connection] == false) {
|
||||
this->HandleAuthentication(connection, request);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string name = request[message::name];
|
||||
std::string id = request[message::id];
|
||||
|
||||
@ -359,8 +399,8 @@ class PlaybackRemote : public IPlaybackRemote {
|
||||
std::string str = msg.dump();
|
||||
|
||||
ReadLock rl(::stateMutex);
|
||||
for (connection_hdl connection : this->connections) {
|
||||
wss.send(connection, str.c_str(), websocketpp::frame::opcode::text);
|
||||
for (const auto &keyValue : this->connections) {
|
||||
wss.send(keyValue.first, str.c_str(), websocketpp::frame::opcode::text);
|
||||
}
|
||||
}
|
||||
|
||||
@ -787,6 +827,15 @@ class PlaybackRemote : public IPlaybackRemote {
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetPreferenceString(
|
||||
IPreferences* prefs,
|
||||
const std::string& key,
|
||||
const std::string& defaultValue)
|
||||
{
|
||||
prefs->GetString(key.c_str(), threadLocalBuffer, sizeof(threadLocalBuffer), defaultValue.c_str());
|
||||
return std::string(threadLocalBuffer);
|
||||
}
|
||||
|
||||
template <typename MetadataT>
|
||||
std::string GetMetadataString(MetadataT* metadata, const std::string& key) {
|
||||
metadata->GetValue(key.c_str(), threadLocalBuffer, sizeof(threadLocalBuffer));
|
||||
@ -835,7 +884,7 @@ class PlaybackRemote : public IPlaybackRemote {
|
||||
|
||||
void OnOpen(connection_hdl connection) {
|
||||
WriteLock wl(::stateMutex);
|
||||
connections.insert(connection);
|
||||
connections[connection] = false;
|
||||
}
|
||||
|
||||
void OnClose(connection_hdl connection) {
|
||||
@ -859,7 +908,7 @@ class PlaybackRemote : public IPlaybackRemote {
|
||||
}
|
||||
}
|
||||
|
||||
connection_list connections;
|
||||
ConnectionList connections;
|
||||
std::shared_ptr<std::thread> thread;
|
||||
server wss;
|
||||
|
||||
@ -879,6 +928,12 @@ extern "C" DLL_EXPORT IPlaybackRemote* GetPlaybackRemote() {
|
||||
extern "C" DLL_EXPORT void SetPreferences(musik::core::sdk::IPreferences* prefs) {
|
||||
WriteLock wl(::stateMutex);
|
||||
::preferences = prefs;
|
||||
|
||||
if (prefs) {
|
||||
prefs->GetInt(key::port.c_str(), DEFAULT_PORT);
|
||||
prefs->GetString(key::password.c_str(), nullptr, 0, DEFAULT_PASSWORD);
|
||||
}
|
||||
|
||||
remote.CheckRunningStatus();
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,8 @@ namespace musik { namespace core { namespace sdk {
|
||||
virtual void SetInt(const char* key, int value) = 0;
|
||||
virtual void SetDouble(const char* key, double value) = 0;
|
||||
virtual void SetString(const char* key, const char* value) = 0;
|
||||
|
||||
virtual void Save() = 0;
|
||||
};
|
||||
|
||||
} } }
|
||||
|
@ -72,6 +72,8 @@ namespace musik { namespace core {
|
||||
virtual void SetDouble(const char* key, double value);
|
||||
virtual void SetString(const char* key, const char* value);
|
||||
|
||||
virtual void Save();
|
||||
|
||||
/* easier interface for internal use */
|
||||
virtual bool GetBool(const std::string& key, bool defaultValue = false);
|
||||
virtual int GetInt(const std::string& key, int defaultValue = 0);
|
||||
@ -84,7 +86,6 @@ namespace musik { namespace core {
|
||||
virtual void SetString(const std::string& key, const char* value);
|
||||
|
||||
void GetKeys(std::vector<std::string>& target);
|
||||
void Save();
|
||||
|
||||
private:
|
||||
Preferences(const std::string& component, Mode mode);
|
||||
|
@ -108,7 +108,10 @@ public class AlbumBrowseActivity extends WebSocketActivityBase implements Filter
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(SocketMessage message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidPassword() {
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -108,7 +108,10 @@ public class CategoryBrowseActivity extends WebSocketActivityBase implements Fil
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(SocketMessage message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidPassword() {
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
package io.casey.musikcube.remote;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
|
||||
public class InvalidPasswordDialogFragment extends DialogFragment {
|
||||
public static final String TAG = "InvalidPasswordDialogFragment";
|
||||
|
||||
public static InvalidPasswordDialogFragment newInstance() {
|
||||
return new InvalidPasswordDialogFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
return new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.invalid_password_dialog_title)
|
||||
.setMessage(R.string.invalid_password_dialog_message)
|
||||
.setPositiveButton(R.string.button_settings, (dialog, which) -> {
|
||||
startActivity(SettingsActivity.getStartIntent(getActivity()));
|
||||
})
|
||||
.setNegativeButton(R.string.button_close, null)
|
||||
.create();
|
||||
}
|
||||
}
|
@ -46,6 +46,7 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
private CheckBox shuffleCb, muteCb, repeatCb;
|
||||
private ImageView albumArtImageView;
|
||||
private View mainTrackMetadataWithAlbumArt, mainTrackMetadataNoAlbumArt;
|
||||
private View disconnectedOverlay;
|
||||
private Handler handler = new Handler();
|
||||
private ViewPropertyAnimator metadataAnim1, metadataAnim2;
|
||||
|
||||
@ -152,6 +153,8 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
this.albumArtImageView = (ImageView) findViewById(R.id.album_art);
|
||||
this.connected = findViewById(R.id.connected);
|
||||
|
||||
this.disconnectedOverlay = findViewById(R.id.disconnected_overlay);
|
||||
|
||||
/* these will get faded in as appropriate */
|
||||
this.mainTrackMetadataNoAlbumArt.setAlpha(0.0f);
|
||||
this.mainTrackMetadataWithAlbumArt.setAlpha(0.0f);
|
||||
@ -208,6 +211,12 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
.build());
|
||||
});
|
||||
|
||||
notPlayingOrDisconnected.setOnClickListener((view) -> {
|
||||
if (wss.getState() != WebSocketService.State.Connected) {
|
||||
wss.reconnect();
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.button_artists).setOnClickListener((View view) -> {
|
||||
startActivity(CategoryBrowseActivity.getStartIntent(this, Messages.Category.ALBUM_ARTIST));
|
||||
});
|
||||
@ -296,6 +305,7 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
final boolean stateIsValidForArtwork = !stopped && connected;
|
||||
|
||||
this.connected.setVisibility((connected && stopped) ? View.VISIBLE : View.GONE);
|
||||
this.disconnectedOverlay.setVisibility(connected ? View.GONE : View.VISIBLE);
|
||||
|
||||
/* setup our state as if we have no album art -- because we don't know if we have any
|
||||
yet! the album art load process (if enabled) will ensure the correct metadata block
|
||||
@ -539,5 +549,14 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidPassword() {
|
||||
final String tag = InvalidPasswordDialogFragment.TAG;
|
||||
if (getSupportFragmentManager().findFragmentByTag(tag) == null) {
|
||||
InvalidPasswordDialogFragment
|
||||
.newInstance().show(getSupportFragmentManager(), tag);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package io.casey.musikcube.remote;
|
||||
|
||||
public class Messages {
|
||||
public enum Request {
|
||||
Authenticate("authenticate"),
|
||||
Ping("ping"),
|
||||
GetPlaybackOverview("get_playback_overview"),
|
||||
PauseOrResume("pause_or_resume"),
|
||||
|
@ -122,6 +122,10 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
||||
updatePlaybackModel(broadcast);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidPassword() {
|
||||
}
|
||||
};
|
||||
|
||||
private class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
@ -13,7 +13,7 @@ import android.widget.EditText;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
private EditText addressText, portText;
|
||||
private EditText addressText, portText, passwordText;
|
||||
private CheckBox albumArtCheckbox;
|
||||
private SharedPreferences prefs;
|
||||
|
||||
@ -33,11 +33,13 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
private void rebindUi() {
|
||||
Views.setTextAndMoveCursorToEnd(this.addressText, prefs.getString("address", "192.168.1.100"));
|
||||
Views.setTextAndMoveCursorToEnd(this.portText, String.format(Locale.ENGLISH, "%d", prefs.getInt("port", 9002)));
|
||||
this.passwordText.setText(prefs.getString("password", ""));
|
||||
}
|
||||
|
||||
private void bindEventListeners() {
|
||||
this.addressText = (EditText) this.findViewById(R.id.address);
|
||||
this.portText = (EditText) this.findViewById(R.id.port);
|
||||
this.passwordText = (EditText) this.findViewById(R.id.password);
|
||||
this.albumArtCheckbox = (CheckBox) findViewById(R.id.album_art_checkbox);
|
||||
|
||||
this.albumArtCheckbox.setChecked(this.prefs.getBoolean("album_art_enabled", true));
|
||||
@ -45,15 +47,16 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
this.findViewById(R.id.button_connect).setOnClickListener((View v) -> {
|
||||
final String addr = addressText.getText().toString();
|
||||
final String port = portText.getText().toString();
|
||||
final String password = passwordText.getText().toString();
|
||||
|
||||
prefs.edit()
|
||||
.putString("address", addr)
|
||||
.putInt("port", (port.length() > 0) ? Integer.valueOf(port) : 0)
|
||||
.putString("password", password)
|
||||
.putBoolean("album_art_enabled", albumArtCheckbox.isChecked())
|
||||
.apply();
|
||||
|
||||
WebSocketService.getInstance(this).disconnect();
|
||||
WebSocketService.getInstance(this).reconnect();
|
||||
|
||||
finish();
|
||||
});
|
||||
|
@ -98,6 +98,10 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
||||
@Override
|
||||
public void onMessageReceived(SocketMessage message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidPassword() {
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onItemClickListener = (View view) -> {
|
||||
|
@ -182,5 +182,9 @@ public class TrackListScrollCache<TrackType> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidPassword() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -144,5 +144,9 @@ public class TransportFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidPassword() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ public class WebSocketService {
|
||||
private static final int PING_INTERVAL_MILLIS = 3500;
|
||||
private static final int AUTO_CONNECT_FAILSAFE_DELAY_MILLIS = 2000;
|
||||
private static final int AUTO_DISCONNECT_DELAY_MILLIS = 5000;
|
||||
private static final int FLAG_AUTHENTICATION_FAILED = 0xbeef;
|
||||
private static final int WEBSOCKET_FLAG_POLICY_VIOLATION = 1008;
|
||||
|
||||
private static final int MESSAGE_BASE = 0xcafedead;
|
||||
private static final int MESSAGE_CONNECT_THREAD_FINISHED = MESSAGE_BASE + 0;
|
||||
@ -45,6 +47,7 @@ public class WebSocketService {
|
||||
public interface Client {
|
||||
void onStateChanged(State newState, State oldState);
|
||||
void onMessageReceived(SocketMessage message);
|
||||
void onInvalidPassword();
|
||||
}
|
||||
|
||||
public interface MessageResultCallback {
|
||||
@ -66,7 +69,14 @@ public class WebSocketService {
|
||||
public boolean handleMessage(Message message) {
|
||||
if (message.what == MESSAGE_CONNECT_THREAD_FINISHED) {
|
||||
if (message.obj == null) {
|
||||
disconnect(true); /* auto-reconnect */
|
||||
boolean invalidPassword = (message.arg1 == FLAG_AUTHENTICATION_FAILED);
|
||||
disconnect(!invalidPassword); /* auto reconnect as long as password was not invalid */
|
||||
|
||||
if (invalidPassword) {
|
||||
for (Client client : clients) {
|
||||
client.onInvalidPassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
setSocket((WebSocket) message.obj);
|
||||
@ -162,6 +172,7 @@ public class WebSocketService {
|
||||
|
||||
if (this.clients.size() == 1) {
|
||||
registerReceiverAndScheduleFailsafe();
|
||||
reconnect();
|
||||
handler.removeCallbacks(autoDisconnectRunnable);
|
||||
}
|
||||
|
||||
@ -327,12 +338,13 @@ public class WebSocketService {
|
||||
}
|
||||
|
||||
private void connectIfNotConnected() {
|
||||
if (state != State.Connected || !socket.isOpen()) {
|
||||
if (state == State.Disconnected) {
|
||||
disconnect(autoReconnect);
|
||||
handler.removeMessages(MESSAGE_AUTO_RECONNECT);
|
||||
setState(State.Connecting);
|
||||
|
||||
if (this.clients.size() > 0) {
|
||||
handler.removeCallbacks(autoDisconnectRunnable);
|
||||
setState(State.Connecting);
|
||||
thread = new ConnectThread();
|
||||
thread.start();
|
||||
}
|
||||
@ -346,7 +358,6 @@ public class WebSocketService {
|
||||
}
|
||||
|
||||
this.socket = socket;
|
||||
this.socket.addListener(webSocketAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -389,7 +400,7 @@ public class WebSocketService {
|
||||
}
|
||||
|
||||
private Runnable autoReconnectFailsafeRunnable = () -> {
|
||||
if (getState() != WebSocketService.State.Connected) {
|
||||
if (autoReconnect && getState() == State.Disconnected) {
|
||||
reconnect();
|
||||
}
|
||||
};
|
||||
@ -401,7 +412,13 @@ public class WebSocketService {
|
||||
public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
|
||||
final SocketMessage message = SocketMessage.create(frame.getPayloadText());
|
||||
if (message != null) {
|
||||
handler.sendMessage(Message.obtain(handler, MESSAGE_MESSAGE_RECEIVED, message));
|
||||
if (message.getName().equals(Messages.Request.Authenticate.toString())) {
|
||||
handler.sendMessage(Message.obtain(
|
||||
handler, MESSAGE_CONNECT_THREAD_FINISHED, websocket));
|
||||
}
|
||||
else {
|
||||
handler.sendMessage(Message.obtain(handler, MESSAGE_MESSAGE_RECEIVED, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -410,7 +427,12 @@ public class WebSocketService {
|
||||
WebSocketFrame serverCloseFrame,
|
||||
WebSocketFrame clientCloseFrame,
|
||||
boolean closedByServer) throws Exception {
|
||||
handler.sendMessage(Message.obtain(handler, MESSAGE_CONNECT_THREAD_FINISHED, null));
|
||||
int flags = 0;
|
||||
if (serverCloseFrame.getCloseCode() == WEBSOCKET_FLAG_POLICY_VIOLATION) {
|
||||
flags = FLAG_AUTHENTICATION_FAILED;
|
||||
}
|
||||
|
||||
handler.sendMessage(Message.obtain(handler, MESSAGE_CONNECT_THREAD_FINISHED, flags, 0, null));
|
||||
}
|
||||
};
|
||||
|
||||
@ -429,17 +451,27 @@ public class WebSocketService {
|
||||
prefs.getInt("port", 9002));
|
||||
|
||||
socket = factory.createSocket(host, CONNECTION_TIMEOUT_MILLIS);
|
||||
socket.addListener(webSocketAdapter);
|
||||
socket.connect();
|
||||
socket.setPingInterval(PING_INTERVAL_MILLIS);
|
||||
|
||||
/* authenticate */
|
||||
final String auth = SocketMessage.Builder
|
||||
.request(Messages.Request.Authenticate)
|
||||
.addOption("password", prefs.getString("password", ""))
|
||||
.build()
|
||||
.toString();
|
||||
|
||||
socket.sendText(auth);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
socket = null;
|
||||
}
|
||||
|
||||
synchronized (WebSocketService.this) {
|
||||
if (!isInterrupted()) {
|
||||
if (thread == this && socket == null) {
|
||||
handler.sendMessage(Message.obtain(
|
||||
handler, MESSAGE_CONNECT_THREAD_FINISHED, socket));
|
||||
handler, MESSAGE_CONNECT_THREAD_FINISHED, null));
|
||||
}
|
||||
|
||||
if (thread == this) {
|
||||
@ -458,9 +490,8 @@ public class WebSocketService {
|
||||
final NetworkInfo info = cm.getActiveNetworkInfo();
|
||||
|
||||
if (info != null && info.isConnected()) {
|
||||
if (getState() == WebSocketService.State.Disconnected) {
|
||||
disconnect();
|
||||
reconnect();
|
||||
if (autoReconnect) {
|
||||
connectIfNotConnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -469,5 +500,6 @@ public class WebSocketService {
|
||||
private static Client INTERNAL_CLIENT = new Client() {
|
||||
public void onStateChanged(State newState, State oldState) { }
|
||||
public void onMessageReceived(SocketMessage message) { }
|
||||
public void onInvalidPassword() { }
|
||||
};
|
||||
}
|
||||
|
@ -146,209 +146,222 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
<FrameLayout
|
||||
android:id="@+id/play_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp" />
|
||||
android:layout_alignParentBottom="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="@style/BrowseButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_artists"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_artists"/>
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/BrowseButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_albums"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_albums"/>
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
<TextView
|
||||
style="@style/BrowseButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_artists"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_artists"/>
|
||||
|
||||
<TextView
|
||||
style="@style/BrowseButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_tracks"
|
||||
android:layout_width="0dp"
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/BrowseButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_albums"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_albums"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/BrowseButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_tracks"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_tracks"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/BrowseButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_play_queue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_play_queue"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_tracks"/>
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
<TextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_prev"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_prev"/>
|
||||
|
||||
<TextView
|
||||
style="@style/BrowseButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_play_queue"
|
||||
android:layout_width="0dp"
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_play_pause"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_play"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_next"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_play_queue"/>
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<io.casey.musikcube.remote.LongPressTextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_vol_down"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_vol_down"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<io.casey.musikcube.remote.LongPressTextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_seek_back"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_seek_back"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<io.casey.musikcube.remote.LongPressTextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_seek_forward"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_seek_forward"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<io.casey.musikcube.remote.LongPressTextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_vol_up"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_vol_up"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<CheckBox
|
||||
style="@style/PlaybackCheckbox"
|
||||
android:id="@+id/check_shuffle"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_shuffle"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<CheckBox
|
||||
style="@style/PlaybackCheckbox"
|
||||
android:id="@+id/check_mute"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_mute"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<CheckBox
|
||||
style="@style/PlaybackCheckbox"
|
||||
android:id="@+id/check_repeat"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_repeat_off"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
<FrameLayout
|
||||
android:id="@+id/disconnected_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/theme_button_background_transparent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_prev"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_prev"/>
|
||||
</FrameLayout>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_play_pause"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_play"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_next"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<io.casey.musikcube.remote.LongPressTextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_vol_down"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_vol_down"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<io.casey.musikcube.remote.LongPressTextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_seek_back"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_seek_back"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<io.casey.musikcube.remote.LongPressTextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_seek_forward"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_seek_forward"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<io.casey.musikcube.remote.LongPressTextView
|
||||
style="@style/PlaybackButton"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/button_vol_up"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_vol_up"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<CheckBox
|
||||
style="@style/PlaybackCheckbox"
|
||||
android:id="@+id/check_shuffle"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/button_shuffle"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<CheckBox
|
||||
style="@style/PlaybackCheckbox"
|
||||
android:id="@+id/check_mute"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_mute"/>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<CheckBox
|
||||
style="@style/PlaybackCheckbox"
|
||||
android:id="@+id/check_repeat"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_repeat_off"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -56,6 +56,22 @@
|
||||
|
||||
</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/password"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:hint="@string/edit_connection_password"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="8dp"/>
|
||||
|
@ -19,6 +19,10 @@
|
||||
<string name="button_repeat_list">repeat list</string>
|
||||
<string name="button_repeat_track">repeat song</string>
|
||||
<string name="button_save">save</string>
|
||||
<string name="button_settings">settings</string>
|
||||
<string name="button_close">close</string>
|
||||
<string name="invalid_password_dialog_title">invalid password</string>
|
||||
<string name="invalid_password_dialog_message">the server rejected your password.\n\nchange the password in the settings screen.</string>
|
||||
<string name="status_connecting">connecting</string>
|
||||
<string name="status_disconnected">disconnected</string>
|
||||
<string name="status_connected">connected to %1$s</string>
|
||||
@ -26,6 +30,7 @@
|
||||
<string name="edit_connection_info">connection info:</string>
|
||||
<string name="edit_connection_hostname">ip address or hostname</string>
|
||||
<string name="edit_connection_port">port</string>
|
||||
<string name="edit_connection_password">password (default empty)</string>
|
||||
<string name="transport_not_playing">not playing</string>
|
||||
<string name="search_hint">search</string>
|
||||
<string name="menu_settings">settings</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user