- Upgraded AndroidVideoCache to a custom build that fixes bugs related

to seeking while streaming when the backend returns an HTTP 200
  instead of a 206 (i.e. ignores a range request)
- Removed Vol+, Vol-, Seek>, and <Seek buttons in favor of current and
  total time controls with a seekbar.
- Removed LongPressTextView
- Added Snackbar notifications when switching streaming modes
This commit is contained in:
casey langen 2017-06-04 00:20:56 -07:00
parent a613022209
commit 3051ebf813
14 changed files with 223 additions and 208 deletions

View File

@ -5,11 +5,15 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.SeekBar;
import android.widget.TextView;
import java.util.HashMap;
@ -26,7 +30,6 @@ import io.casey.musikcube.remote.ui.activity.TrackListActivity;
import io.casey.musikcube.remote.ui.activity.WebSocketActivityBase;
import io.casey.musikcube.remote.ui.fragment.InvalidPasswordDialogFragment;
import io.casey.musikcube.remote.ui.util.Views;
import io.casey.musikcube.remote.ui.view.LongPressTextView;
import io.casey.musikcube.remote.ui.view.MainMetadataView;
import io.casey.musikcube.remote.util.Duration;
import io.casey.musikcube.remote.websocket.Messages;
@ -43,11 +46,14 @@ public class MainActivity extends WebSocketActivityBase {
private SharedPreferences prefs;
private PlaybackService playback;
private View mainLayout;
private MainMetadataView metadataView;
private TextView playPause, currentTime, totalTime;
private View connectedNotPlaying, disconnectedButton;
private CheckBox shuffleCb, muteCb, repeatCb;
private View disconnectedOverlay;
private SeekBar seekbar;
private int seekbarValue = -1;
static {
REPEAT_TO_STRING_ID = new HashMap<>();
@ -169,6 +175,12 @@ public class MainActivity extends WebSocketActivityBase {
prefs.edit().putBoolean(Prefs.Key.STREAMING_PLAYBACK, !streaming).apply();
final int messageId = streaming
? R.string.snackbar_remote_enabled
: R.string.snackbar_streaming_enabled;
showSnackbar(messageId);
reloadPlaybackService();
this.playback = getPlaybackService();
@ -176,6 +188,15 @@ public class MainActivity extends WebSocketActivityBase {
rebindUi();
}
private void showSnackbar(int stringId) {
final Snackbar sb = Snackbar.make(mainLayout, stringId, Snackbar.LENGTH_LONG);
final View sbView = sb.getView();
sbView.setBackgroundColor(ContextCompat.getColor(this, R.color.color_primary));
final TextView tv = (TextView) sbView.findViewById(android.support.design.R.id.snackbar_text);
tv.setTextColor(ContextCompat.getColor(this, R.color.theme_foreground));
sb.show();
}
private void bindCheckBoxEventListeners() {
this.shuffleCb.setOnCheckedChangeListener(shuffleListener);
this.muteCb.setOnCheckedChangeListener(muteListener);
@ -191,6 +212,7 @@ public class MainActivity extends WebSocketActivityBase {
}
private void bindEventListeners() {
this.mainLayout = findViewById(R.id.activity_main);
this.metadataView = (MainMetadataView) findViewById(R.id.main_metadata_view);
this.shuffleCb = (CheckBox) findViewById(R.id.check_shuffle);
this.muteCb = (CheckBox) findViewById(R.id.check_mute);
@ -201,12 +223,10 @@ public class MainActivity extends WebSocketActivityBase {
this.playPause = (TextView) findViewById(R.id.button_play_pause);
this.currentTime = (TextView) findViewById(R.id.current_time);
this.totalTime = (TextView) findViewById(R.id.total_time);
this.seekbar = (SeekBar) findViewById(R.id.seekbar);
findViewById(R.id.button_prev).setOnClickListener((View view) -> playback.prev());
final LongPressTextView seekBack = (LongPressTextView) findViewById(R.id.button_seek_back);
seekBack.setOnTickListener((View view) -> playback.seekBackward());
findViewById(R.id.button_play_pause).setOnClickListener((View view) -> {
if (playback.getPlaybackState() == PlaybackState.Stopped) {
playback.playAll();
@ -218,19 +238,33 @@ public class MainActivity extends WebSocketActivityBase {
findViewById(R.id.button_next).setOnClickListener((View view) -> playback.next());
final LongPressTextView seekForward = (LongPressTextView) findViewById(R.id.button_seek_forward);
seekForward.setOnTickListener((View view) -> playback.seekForward());
final LongPressTextView volumeUp = (LongPressTextView) findViewById(R.id.button_vol_up);
volumeUp.setOnTickListener((View view) -> playback.volumeUp());
final LongPressTextView volumeDown = (LongPressTextView) findViewById(R.id.button_vol_down);
volumeDown.setOnTickListener((View view) -> playback.volumeDown());
disconnectedButton.setOnClickListener((view) -> {
wss.reconnect();
});
seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
seekbarValue = progress;
currentTime.setText(Duration.format(seekbarValue));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (seekbarValue != -1) {
playback.seekTo((double) seekbarValue);
seekbarValue = -1;
}
}
});
findViewById(R.id.button_artists).setOnClickListener((View view) -> {
startActivity(CategoryBrowseActivity.getStartIntent(this, Messages.Category.ALBUM_ARTIST));
});
@ -265,10 +299,11 @@ public class MainActivity extends WebSocketActivityBase {
final boolean connected = (wss.getState() == WebSocketService.State.Connected);
final boolean stopped = (playback.getPlaybackState() == PlaybackState.Stopped);
final boolean playing = (playback.getPlaybackState() == PlaybackState.Playing);
final boolean buffering = (playback.getPlaybackState() == PlaybackState.Buffering);
final boolean showMetadataView = !stopped && connected && playback.getQueueCount() > 0;
/* bottom section: transport controls */
this.playPause.setText(playing ? R.string.button_pause : R.string.button_play);
this.playPause.setText(playing || buffering ? R.string.button_pause : R.string.button_play);
this.connectedNotPlaying.setVisibility((connected && stopped) ? View.VISIBLE : View.GONE);
this.disconnectedOverlay.setVisibility(connected ? View.GONE : View.VISIBLE);
@ -317,8 +352,15 @@ public class MainActivity extends WebSocketActivityBase {
}
private Runnable updateTimeRunnable = () -> {
currentTime.setText(Duration.format(playback.getCurrentTime()));
totalTime.setText(Duration.format(playback.getDuration()));
final double duration = playback.getDuration();
final double current = (seekbarValue == -1) ? playback.getCurrentTime() : seekbarValue;
currentTime.setText(Duration.format(current));
totalTime.setText(Duration.format(duration));
seekbar.setMax((int) duration);
seekbar.setProgress((int) current);
scheduleUpdateTime(false);
};

View File

@ -1,7 +1,9 @@
package io.casey.musikcube.remote.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Base64;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
@ -9,6 +11,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
@ -24,10 +27,19 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import io.casey.musikcube.remote.Application;
import io.casey.musikcube.remote.util.NetworkUtil;
import io.casey.musikcube.remote.util.Preconditions;
import io.casey.musikcube.remote.websocket.Prefs;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import okhttp3.Request;
public class ExoPlayerWrapper extends PlayerWrapper {
private static OkHttpClient audioStreamHttpClient = null;
private DataSource.Factory datasources;
private ExtractorsFactory extractors;
private MediaSource source;
@ -35,7 +47,56 @@ public class ExoPlayerWrapper extends PlayerWrapper {
private boolean prefetch;
private Context context;
private long lastPosition = -1;
private String uri, proxyUri;
private String originalUri, resolvedUri;
private void initHttpClient(final String uri) {
if (StreamProxy.ENABLED) {
return;
}
synchronized (ExoPlayerWrapper.class) {
if (audioStreamHttpClient == null) {
final SharedPreferences prefs = Application.getInstance()
.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
final File path = new File(context.getExternalCacheDir(), "audio");
int diskCacheIndex = prefs.getInt(
Prefs.Key.DISK_CACHE_SIZE_INDEX, Prefs.Default.DISK_CACHE_SIZE_INDEX);
if (diskCacheIndex < 0 || diskCacheIndex > StreamProxy.CACHE_SETTING_TO_BYTES.size()) {
diskCacheIndex = 0;
}
final OkHttpClient.Builder builder = new OkHttpClient.Builder()
.cache(new Cache(path, StreamProxy.CACHE_SETTING_TO_BYTES.get(diskCacheIndex)))
.addInterceptor((chain) -> {
Request request = chain.request();
final String userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD);
final String encoded = Base64.encodeToString(userPass.getBytes(), Base64.NO_WRAP);
request = request.newBuilder().addHeader("Authorization", "Basic " + encoded).build();
return chain.proceed(request);
});
if (prefs.getBoolean(Prefs.Key.CERT_VALIDATION_DISABLED, Prefs.Default.CERT_VALIDATION_DISABLED)) {
NetworkUtil.disableCertificateValidation(builder);
}
audioStreamHttpClient = builder.build();
}
}
if (uri.startsWith("http")) {
this.datasources = new OkHttpDataSourceFactory(
audioStreamHttpClient,
Util.getUserAgent(context, "musikdroid"),
new DefaultBandwidthMeter());
}
else {
this.datasources = new DefaultDataSourceFactory(
context, Util.getUserAgent(context, "musikdroid"));
}
}
public ExoPlayerWrapper() {
this.context = Application.getInstance();
@ -53,9 +114,10 @@ public class ExoPlayerWrapper extends PlayerWrapper {
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
this.uri = uri;
this.proxyUri = StreamProxy.getProxyUrl(context, uri);
this.source = new ExtractorMediaSource(Uri.parse(proxyUri), datasources, extractors, null, null);
initHttpClient(uri);
this.originalUri = uri;
this.resolvedUri = StreamProxy.getProxyUrl(context, uri);
this.source = new ExtractorMediaSource(Uri.parse(resolvedUri), datasources, extractors, null, null);
this.player.setPlayWhenReady(true);
this.player.prepare(this.source);
addActivePlayer(this);
@ -68,10 +130,11 @@ public class ExoPlayerWrapper extends PlayerWrapper {
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
this.uri = uri;
initHttpClient(uri);
this.originalUri = uri;
this.prefetch = true;
this.proxyUri = StreamProxy.getProxyUrl(context, uri);
this.source = new ExtractorMediaSource(Uri.parse(proxyUri), datasources, extractors, null, null);
this.resolvedUri = StreamProxy.getProxyUrl(context, uri);
this.source = new ExtractorMediaSource(Uri.parse(resolvedUri), datasources, extractors, null, null);
this.player.setPlayWhenReady(false);
this.player.prepare(this.source);
addActivePlayer(this);
@ -119,6 +182,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
this.lastPosition = -1;
if (this.player.getPlaybackState() != ExoPlayer.STATE_IDLE) {
if (this.player.isCurrentWindowSeekable()) {
this.lastPosition = millis;
this.player.seekTo(millis);
}
}
@ -196,7 +260,10 @@ public class ExoPlayerWrapper extends PlayerWrapper {
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Preconditions.throwIfNotOnMainThread();
if (playbackState == ExoPlayer.STATE_READY) {
if (playbackState == ExoPlayer.STATE_BUFFERING) {
setState(State.Buffering);
}
else if (playbackState == ExoPlayer.STATE_READY) {
if (dead()) {
dispose();
}

View File

@ -1,15 +1,21 @@
package io.casey.musikcube.remote.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.PowerManager;
import android.util.Base64;
import android.util.Log;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import io.casey.musikcube.remote.Application;
import io.casey.musikcube.remote.util.Preconditions;
import io.casey.musikcube.remote.websocket.Prefs;
public class MediaPlayerWrapper extends PlayerWrapper {
private static final String TAG = "MediaPlayerWrapper";
@ -17,6 +23,12 @@ public class MediaPlayerWrapper extends PlayerWrapper {
private MediaPlayer player = new MediaPlayer();
private int seekTo;
private boolean prefetching;
private Context context = Application.getInstance();
private SharedPreferences prefs;
public MediaPlayerWrapper() {
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
}
@Override
public void play(final String uri) {
@ -24,7 +36,17 @@ public class MediaPlayerWrapper extends PlayerWrapper {
try {
setState(State.Preparing);
player.setDataSource(Application.getInstance(), Uri.parse(uri));
final String userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD);
final String encoded = Base64.encodeToString(userPass.getBytes(), Base64.NO_WRAP);
final Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Basic " + encoded);
player.setDataSource(
context,
Uri.parse(StreamProxy.getProxyUrl(context, uri)),
headers);
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setOnPreparedListener(onPrepared);
player.setOnErrorListener(onError);

View File

@ -32,8 +32,10 @@ public interface PlaybackService {
void volumeUp();
void volumeDown();
void seekForward();
void seekBackward();
void seekTo(double seconds);
int getQueueCount();
int getQueuePosition();

View File

@ -1,14 +1,14 @@
package io.casey.musikcube.remote.playback;
import android.util.Log;
import java.util.HashSet;
import java.util.Set;
import io.casey.musikcube.remote.util.Preconditions;
public abstract class PlayerWrapper {
private static final String TAG = "MediaPlayerWrapper";
private enum Type { MediaPlayer, ExoPlayer }
private static final Type TYPE = Type.ExoPlayer;
private static final float DUCK_COEF = 0.2f; /* volume = 20% when ducked */
private static final float DUCK_NONE = -1.0f;
@ -17,6 +17,7 @@ public abstract class PlayerWrapper {
Preparing,
Prepared,
Playing,
Buffering,
Paused,
Error,
Finished,
@ -92,8 +93,9 @@ public abstract class PlayerWrapper {
}
public static PlayerWrapper newInstance() {
//return new MediaPlayerWrapper();
return new ExoPlayerWrapper();
return TYPE == Type.ExoPlayer
? new ExoPlayerWrapper()
: new MediaPlayerWrapper();
}
protected static void addActivePlayer(final PlayerWrapper player) {

View File

@ -209,6 +209,15 @@ public class RemotePlaybackService implements PlaybackService {
.addOption(Messages.Key.DELTA, -5.0f).build());
}
@Override
public void seekTo(double seconds) {
wss.send(SocketMessage.Builder
.request(Messages.Request.SeekTo)
.addOption(Messages.Key.POSITION, seconds).build());
currentTime.update(seconds, currentTime.trackId);
}
@Override
public int getQueueCount() {
return queueCount;

View File

@ -19,9 +19,10 @@ import io.casey.musikcube.remote.util.NetworkUtil;
import io.casey.musikcube.remote.websocket.Prefs;
public class StreamProxy {
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;
public static final boolean ENABLED = true;
public static final long BYTES_PER_MEGABYTE = 1048576L;
public static final long BYTES_PER_GIGABYTE = 1073741824L;
public static final Map<Integer, Long> CACHE_SETTING_TO_BYTES;
private static final FileNameGenerator DEFAULT_FILENAME_GENERATOR = new Md5FileNameGenerator();
static {
@ -105,7 +106,7 @@ public class StreamProxy {
public static synchronized String getProxyUrl(final Context context, final String url) {
init(context);
return INSTANCE.proxy.getProxyUrl(url);
return ENABLED ? INSTANCE.proxy.getProxyUrl(url) : url;
}
public static synchronized void reload() {

View File

@ -215,7 +215,7 @@ public class StreamingPlaybackService implements PlaybackService {
@Override
public void pauseOrResume() {
if (context.currentPlayer != null) {
if (state == PlaybackState.Playing) {
if (state == PlaybackState.Playing || state == PlaybackState.Buffering) {
pause();
}
else {
@ -308,6 +308,15 @@ public class StreamingPlaybackService implements PlaybackService {
}
}
@Override
public void seekTo(double seconds) {
if (requestAudioFocus()) {
if (context.currentPlayer != null) {
context.currentPlayer.setPosition((int)(seconds * 1000));
}
}
}
@Override
public int getQueueCount() {
return context.queueCount;
@ -508,6 +517,10 @@ public class StreamingPlaybackService implements PlaybackService {
precacheTrackMetadata(context.currentIndex, PRECACHE_METADATA_SIZE);
break;
case Buffering:
setState(PlaybackState.Buffering);
break;
case Paused:
pause();
break;

View File

@ -1,99 +0,0 @@
package io.casey.musikcube.remote.ui.view;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
public class LongPressTextView extends TextView {
private static final int TICK_START_DELAY = 700;
private static final int MINIMUM_TICK_DELAY = 100;
private static final int TICK_DELTA = 100;
private static final int FIRST_TICK_DELAY = 200;
public interface OnTickListener {
void onTick(final View view);
}
private int tickDelay = 0;
private int ticksFired = 0;
private boolean isDown;
private Handler handler = new Handler();
private OnTickListener onTickListener;
private View.OnClickListener onClickListener;
public LongPressTextView(Context context) {
super(context);
init();
}
public LongPressTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public LongPressTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void setOnTickListener(OnTickListener onTickListener) {
this.onTickListener = onTickListener;
this.setClickable(onTickListener != null);
}
@Override
public void setOnClickListener(OnClickListener l) {
this.onClickListener = l;
}
private void init() {
super.setOnClickListener(onClickProxy);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
isDown = true;
ticksFired = 0;
tickDelay = TICK_START_DELAY;
handler.removeCallbacks(tickRunnable);
handler.postDelayed(tickRunnable, FIRST_TICK_DELAY);
}
else if (event.getAction() == MotionEvent.ACTION_UP) {
handler.removeCallbacks(tickRunnable);
ticksFired = 0;
isDown = false;
}
return super.onTouchEvent(event);
}
private Runnable tickRunnable = new Runnable() {
@Override
public void run() {
if (isDown) {
if (onTickListener != null) {
onTickListener.onTick(LongPressTextView.this);
}
tickDelay = Math.max(MINIMUM_TICK_DELAY, tickDelay - TICK_DELTA);
handler.postDelayed(tickRunnable, tickDelay);
}
}
};
private View.OnClickListener onClickProxy = new OnClickListener() {
@Override
public void onClick(View view) {
if (onTickListener == null && onClickListener != null) {
onClickListener.onClick(view);
}
else if (onTickListener != null && ticksFired == 0) {
onTickListener.onTick(view);
}
}
};
}

View File

@ -94,6 +94,7 @@ public class Messages {
String LIMIT = "limit";
String INDEX = "index";
String DELTA = "delta";
String POSITION = "position";
String VALUE = "value";
String FILTER = "filter";
String RELATIVE = "relative";

View File

@ -22,37 +22,6 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v4.widget.Space
android:layout_width="0dp"
android:layout_height="2dp" />
<FrameLayout
android:background="@drawable/playback_button"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:gravity="left"
android:text="0:00"/>
<TextView
android:id="@+id/total_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right"
android:text="0:00"/>
</FrameLayout>
<android.support.v4.widget.Space
android:layout_width="0dp"
android:layout_height="2dp" />
@ -155,55 +124,39 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="@drawable/playback_button"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:layout_marginTop="2dp"
android:orientation="horizontal">
android:clipToPadding="false"
android:clipChildren="false"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<io.casey.musikcube.remote.ui.view.LongPressTextView
style="@style/PlaybackButton"
android:layout_weight="1.0"
android:id="@+id/button_vol_down"
<TextView
android:id="@+id/current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:gravity="center"
android:text="0:00"/>
<SeekBar
android:id="@+id/seekbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/button_vol_down"/>
android:layout_gravity="center"
android:layout_weight="1.0"/>
<android.support.v4.widget.Space
android:layout_width="2dp"
android:layout_height="0dp" />
<io.casey.musikcube.remote.ui.view.LongPressTextView
style="@style/PlaybackButton"
android:layout_weight="1.0"
android:id="@+id/button_seek_back"
android:layout_width="0dp"
<TextView
android:id="@+id/total_time"
android:layout_width="wrap_content"
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.ui.view.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.ui.view.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"/>
android:minWidth="48dp"
android:gravity="center"
android:text="0:00"/>
</LinearLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -54,6 +54,8 @@
<string name="menu_playlists">playlists</string>
<string name="menu_remote_toggle">remote playback</string>
<string name="unknown_value">&lt;unknown&gt;</string>
<string name="snackbar_streaming_enabled">switched to streaming mode</string>
<string name="snackbar_remote_enabled">switched to remote control mode</string>
<string name="settings_playback_mode">playback mode:</string>
<string name="settings_transcoder_bitrate">streaming downsampler bitrate:</string>
<string name="settings_cache_size">streaming disk cache size:</string>