mirror of
https://github.com/clangen/musikcube.git
synced 2025-01-29 21:32:41 +00:00
Added buffering indicator to the android client, and also tweaked a
couple transcoder settings keys. Also updated some variable names -- we're not decoding, we're encoding!
This commit is contained in:
parent
45811b90a0
commit
0aff544712
@ -41,18 +41,18 @@ namespace defaults {
|
||||
static const int websocket_server_port = 7905;
|
||||
static const int http_server_port = 7906;
|
||||
static const std::string password = "";
|
||||
static const int http_server_transcoder_cache_count = 50;
|
||||
static const bool http_server_transcoder_synchronous = false;
|
||||
static const bool http_server_transcoder_synchronous_fallback = true;
|
||||
static const int transcoder_cache_count = 50;
|
||||
static const bool transcoder_synchronous = false;
|
||||
static const bool transcoder_synchronous_fallback = true;
|
||||
}
|
||||
|
||||
namespace prefs {
|
||||
static const std::string websocket_server_port = "websocket_server_port";
|
||||
static const std::string http_server_enabled = "http_server_enabled";
|
||||
static const std::string http_server_port = "http_server_port";
|
||||
static const std::string http_server_transcoder_cache_count = "http_server_transcoder_cache_count";
|
||||
static const std::string http_server_transcoder_synchronous = "http_server_transcoder_synchronous";
|
||||
static const std::string http_server_transcoder_synchronous_fallback = "http_server_transcoder_synchronous_fallback";
|
||||
static const std::string transcoder_cache_count = "transcoder_cache_count";
|
||||
static const std::string transcoder_synchronous = "transcoder_synchronous";
|
||||
static const std::string transcoder_synchronous_fallback = "transcoder_synchronous_fallback";
|
||||
}
|
||||
|
||||
namespace message {
|
||||
|
@ -328,8 +328,8 @@ int HttpServer::HandleRequest(
|
||||
}
|
||||
|
||||
if (false && server->context.prefs->GetBool(
|
||||
prefs::http_server_transcoder_synchronous_fallback.c_str(),
|
||||
defaults::http_server_transcoder_synchronous_fallback))
|
||||
prefs::transcoder_synchronous_fallback.c_str(),
|
||||
defaults::transcoder_synchronous_fallback))
|
||||
{
|
||||
/* if we're allowed, fall back to synchronous transcoding. we'll block
|
||||
here until the entire file has been converted and cached */
|
||||
|
@ -46,17 +46,15 @@ static std::string cachePath(Context& context) {
|
||||
context.environment->GetPath(PathType::PathData, buf, sizeof(buf));
|
||||
std::string path = std::string(buf) + "/transcode/";
|
||||
|
||||
if (!boost::filesystem::exists(path)) {
|
||||
boost::filesystem::create_directories(path);
|
||||
if (!exists(path)) {
|
||||
create_directories(path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
static void iterateTranscodeCache(Context& context, std::function<void(boost::filesystem::path)> cb) {
|
||||
static void iterateTranscodeCache(Context& context, std::function<void(path)> cb) {
|
||||
if (cb) {
|
||||
using namespace boost::filesystem;
|
||||
|
||||
directory_iterator end;
|
||||
directory_iterator file(cachePath(context));
|
||||
|
||||
@ -70,32 +68,32 @@ static void iterateTranscodeCache(Context& context, std::function<void(boost::fi
|
||||
}
|
||||
|
||||
void Transcoder::RemoveTempTranscodeFiles(Context& context) {
|
||||
iterateTranscodeCache(context, [](boost::filesystem::path p) {
|
||||
iterateTranscodeCache(context, [](path p) {
|
||||
if (p.extension().string() == ".tmp") {
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(p, ec);
|
||||
remove(p, ec);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Transcoder::PruneTranscodeCache(Context& context) {
|
||||
std::map<time_t, boost::filesystem::path> sorted;
|
||||
std::map<time_t, path> sorted;
|
||||
|
||||
boost::system::error_code ec;
|
||||
iterateTranscodeCache(context, [&sorted, &ec](boost::filesystem::path p) {
|
||||
sorted[boost::filesystem::last_write_time(p, ec)] = p;
|
||||
iterateTranscodeCache(context, [&sorted, &ec](path p) {
|
||||
sorted[last_write_time(p, ec)] = p;
|
||||
});
|
||||
|
||||
int maxSize = context.prefs->GetInt(
|
||||
prefs::http_server_transcoder_cache_count.c_str(),
|
||||
defaults::http_server_transcoder_cache_count);
|
||||
prefs::transcoder_cache_count.c_str(),
|
||||
defaults::transcoder_cache_count);
|
||||
|
||||
int extra = (int) sorted.size() - (maxSize - 1);
|
||||
auto it = sorted.begin();
|
||||
while (extra > 0 && it != sorted.end()) {
|
||||
auto p = it->second;
|
||||
boost::system::error_code ec;
|
||||
if (boost::filesystem::remove(p, ec)) {
|
||||
if (remove(p, ec)) {
|
||||
--extra;
|
||||
}
|
||||
}
|
||||
@ -116,15 +114,15 @@ static void getTempAndFinalFilename(
|
||||
|
||||
do {
|
||||
tempFn = finalFn + "." + std::to_string(rand()) + ".tmp";
|
||||
} while (boost::filesystem::exists(tempFn));
|
||||
} while (exists(tempFn));
|
||||
}
|
||||
|
||||
IDataStream* Transcoder::Transcode(
|
||||
Context& context, const std::string& uri, size_t bitrate)
|
||||
{
|
||||
if (context.prefs->GetBool(
|
||||
prefs::http_server_transcoder_synchronous.c_str(),
|
||||
defaults::http_server_transcoder_synchronous))
|
||||
prefs::transcoder_synchronous.c_str(),
|
||||
defaults::transcoder_synchronous))
|
||||
{
|
||||
return TranscodeAndWait(context, uri, bitrate);
|
||||
}
|
||||
|
@ -176,43 +176,43 @@ PositionType TranscodingDataStream::Read(void *buffer, PositionType bytesToRead)
|
||||
size_t numSamples = pcmBuffer->Samples() / pcmBuffer->Channels();
|
||||
size_t requiredBytes = (size_t) (1.25 * (float)numSamples + 7200.0);
|
||||
|
||||
decodedBytes.realloc(requiredBytes);
|
||||
encodedBytes.realloc(requiredBytes);
|
||||
|
||||
/* decode PCM -> MP3 */
|
||||
int decodeCount =
|
||||
/* encode PCM -> MP3 */
|
||||
int encodeCount =
|
||||
lame_encode_buffer_interleaved_ieee_float(
|
||||
lame,
|
||||
pcmBuffer->BufferPointer(),
|
||||
numSamples,
|
||||
decodedBytes.data,
|
||||
decodedBytes.length);
|
||||
encodedBytes.data,
|
||||
encodedBytes.length);
|
||||
|
||||
if (decodeCount < 0) {
|
||||
if (encodeCount < 0) {
|
||||
goto internal_error;
|
||||
}
|
||||
|
||||
decodedBytes.length = (size_t) decodeCount;
|
||||
encodedBytes.length = (size_t)encodeCount;
|
||||
|
||||
/* if we got something, let's write it to the output buffer */
|
||||
if (decodedBytes.length) {
|
||||
if (encodedBytes.length) {
|
||||
size_t toWrite = std::min(
|
||||
decodedBytes.length,
|
||||
encodedBytes.length,
|
||||
(size_t)(bytesToRead - bytesWritten));
|
||||
|
||||
memcpy(dst + bytesWritten, decodedBytes.data, toWrite);
|
||||
memcpy(dst + bytesWritten, encodedBytes.data, toWrite);
|
||||
|
||||
if (this->outFile) {
|
||||
fwrite(decodedBytes.data, 1, toWrite, this->outFile);
|
||||
fwrite(encodedBytes.data, 1, toWrite, this->outFile);
|
||||
}
|
||||
|
||||
decodedBytes.inc(toWrite);
|
||||
encodedBytes.inc(toWrite);
|
||||
bytesWritten += toWrite;
|
||||
|
||||
/* if we have decoded bytes still available, that means the
|
||||
output buffer is exhausted. swap it into the spillover buffer
|
||||
so it can be finalized the next time through. */
|
||||
if (decodedBytes.avail()) {
|
||||
spillover.swap(decodedBytes);
|
||||
if (encodedBytes.avail()) {
|
||||
spillover.swap(encodedBytes);
|
||||
this->position += bytesWritten;
|
||||
return bytesWritten;
|
||||
}
|
||||
@ -228,17 +228,17 @@ PositionType TranscodingDataStream::Read(void *buffer, PositionType bytesToRead)
|
||||
|
||||
/* finalize */
|
||||
if (bytesWritten == 0) {
|
||||
decodedBytes.reset();
|
||||
encodedBytes.reset();
|
||||
|
||||
size_t count = lame_encode_flush(
|
||||
lame,
|
||||
decodedBytes.data,
|
||||
decodedBytes.length);
|
||||
encodedBytes.data,
|
||||
encodedBytes.length);
|
||||
|
||||
memcpy(dst + bytesWritten, decodedBytes.data, count);
|
||||
memcpy(dst + bytesWritten, encodedBytes.data, count);
|
||||
|
||||
if (this->outFile) {
|
||||
fwrite(decodedBytes.data, 1, count, this->outFile);
|
||||
fwrite(encodedBytes.data, 1, count, this->outFile);
|
||||
fclose(this->outFile);
|
||||
this->outFile = nullptr;
|
||||
|
||||
|
@ -128,7 +128,7 @@ class TranscodingDataStream : public musik::core::sdk::IDataStream {
|
||||
size_t offset, length, rawLength;
|
||||
};
|
||||
|
||||
ByteBuffer decodedBytes;
|
||||
ByteBuffer encodedBytes;
|
||||
ByteBuffer spillover;
|
||||
size_t bitrate;
|
||||
bool eof;
|
||||
|
@ -159,9 +159,9 @@ extern "C" DLL_EXPORT void SetPreferences(musik::core::sdk::IPreferences* prefs)
|
||||
prefs->GetInt(prefs::http_server_port.c_str(), defaults::http_server_port);
|
||||
prefs->GetBool(prefs::http_server_enabled.c_str(), true);
|
||||
prefs->GetString(key::password.c_str(), nullptr, 0, defaults::password.c_str());
|
||||
prefs->GetInt(prefs::http_server_transcoder_cache_count.c_str(), defaults::http_server_transcoder_cache_count);
|
||||
prefs->GetBool(prefs::http_server_transcoder_synchronous.c_str(), defaults::http_server_transcoder_synchronous);
|
||||
prefs->GetBool(prefs::http_server_transcoder_synchronous_fallback.c_str(), defaults::http_server_transcoder_synchronous_fallback);
|
||||
prefs->GetInt(prefs::transcoder_cache_count.c_str(), defaults::transcoder_cache_count);
|
||||
prefs->GetBool(prefs::transcoder_synchronous.c_str(), defaults::transcoder_synchronous);
|
||||
prefs->GetBool(prefs::transcoder_synchronous_fallback.c_str(), defaults::transcoder_synchronous_fallback);
|
||||
prefs->Save();
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@
|
||||
#include <core/debug.h>
|
||||
|
||||
static const std::string TAG = "LocalLibrary";
|
||||
static bool scheduleSyncDueToDbUpgrade = false;
|
||||
|
||||
using namespace musik::core;
|
||||
using namespace musik::core::library;
|
||||
@ -90,6 +91,10 @@ LocalLibrary::LocalLibrary(std::string name,int id)
|
||||
this->GetLibraryDirectory(),
|
||||
this->GetDatabaseFilename());
|
||||
|
||||
if (scheduleSyncDueToDbUpgrade) {
|
||||
this->indexer->Schedule(IIndexer::SyncType::Local);
|
||||
}
|
||||
|
||||
this->thread = new std::thread(std::bind(&LocalLibrary::ThreadProc, this));
|
||||
}
|
||||
|
||||
@ -290,6 +295,8 @@ static void upgradeV2ToV3(db::Connection& db) {
|
||||
"name TEXT default '',"
|
||||
"thumbnail_id INTEGER default 0,"
|
||||
"sort_order INTEGER DEFAULT 0)");
|
||||
|
||||
scheduleSyncDueToDbUpgrade = true;
|
||||
}
|
||||
|
||||
static void setVersion(db::Connection& db, int version) {
|
||||
|
@ -59,6 +59,7 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
private TextView title, artist, album, playPause, volume;
|
||||
private TextView titleWithArt, artistAndAlbumWithArt, volumeWithArt;
|
||||
private TextView notPlayingOrDisconnected;
|
||||
private View buffering, bufferingWithArt;
|
||||
private View connected;
|
||||
private CheckBox shuffleCb, muteCb, repeatCb;
|
||||
private View disconnectedOverlay;
|
||||
@ -182,10 +183,12 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
this.artist = (TextView) findViewById(R.id.track_artist);
|
||||
this.album = (TextView) findViewById(R.id.track_album);
|
||||
this.volume = (TextView) findViewById(R.id.volume);
|
||||
this.buffering = findViewById(R.id.buffering);
|
||||
|
||||
this.titleWithArt = (TextView) findViewById(R.id.with_art_track_title);
|
||||
this.artistAndAlbumWithArt = (TextView) findViewById(R.id.with_art_artist_and_album);
|
||||
this.volumeWithArt = (TextView) findViewById(R.id.with_art_volume);
|
||||
this.bufferingWithArt = findViewById(R.id.with_art_buffering);
|
||||
|
||||
this.playPause = (TextView) findViewById(R.id.button_play_pause);
|
||||
this.shuffleCb = (CheckBox) findViewById(R.id.check_shuffle);
|
||||
@ -263,8 +266,13 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
}
|
||||
|
||||
private void rebindAlbumArtistWithArtTextView() {
|
||||
final String artist = playback.getTrackString(Metadata.Track.ARTIST, getString(R.string.unknown_artist));
|
||||
final String album = playback.getTrackString(Metadata.Track.ALBUM, getString(R.string.unknown_album));
|
||||
final boolean buffering = playback.getPlaybackState() == PlaybackState.Buffering;
|
||||
|
||||
final String artist = playback.getTrackString(
|
||||
Metadata.Track.ARTIST, getString(buffering ? R.string.buffering : R.string.unknown_artist));
|
||||
|
||||
final String album = playback.getTrackString(
|
||||
Metadata.Track.ALBUM, getString(buffering ? R.string.buffering : R.string.unknown_album));
|
||||
|
||||
final ForegroundColorSpan albumColor =
|
||||
new ForegroundColorSpan(getResources().getColor(R.color.theme_orange));
|
||||
@ -328,6 +336,8 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
final boolean stopped = (playback.getPlaybackState() == PlaybackState.Stopped);
|
||||
notPlayingOrDisconnected.setVisibility(stopped ? View.VISIBLE : View.GONE);
|
||||
|
||||
boolean buffering = playback.getPlaybackState() == PlaybackState.Buffering;
|
||||
|
||||
final boolean stateIsValidForArtwork = !stopped && connected && playback.getQueueCount() > 0;
|
||||
|
||||
this.connected.setVisibility((connected && stopped) ? View.VISIBLE : View.GONE);
|
||||
@ -350,15 +360,18 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
final String title = playback.getTrackString(Metadata.Track.TITLE, "");
|
||||
final String volume = getString(R.string.status_volume, Math.round(playback.getVolume() * 100));
|
||||
|
||||
this.title.setText(Strings.empty(title) ? getString(R.string.unknown_title) : title);
|
||||
this.artist.setText(Strings.empty(artist) ? getString(R.string.unknown_artist) : artist);
|
||||
this.album.setText(Strings.empty(album) ? getString(R.string.unknown_album) : album);
|
||||
this.title.setText(Strings.empty(title) ? getString(buffering ? R.string.buffering : R.string.unknown_title) : title);
|
||||
this.artist.setText(Strings.empty(artist) ? getString(buffering ? R.string.buffering : R.string.unknown_artist) : artist);
|
||||
this.album.setText(Strings.empty(album) ? getString(buffering ? R.string.buffering : R.string.unknown_album) : album);
|
||||
this.volume.setText(volume);
|
||||
|
||||
this.rebindAlbumArtistWithArtTextView();
|
||||
this.titleWithArt.setText(Strings.empty(title) ? getString(R.string.unknown_title) : title);
|
||||
this.titleWithArt.setText(Strings.empty(title) ? getString(buffering ? R.string.buffering : R.string.unknown_title) : title);
|
||||
this.volumeWithArt.setText(volume);
|
||||
|
||||
this.buffering.setVisibility(buffering ? View.VISIBLE : View.GONE);
|
||||
this.bufferingWithArt.setVisibility(buffering ? View.VISIBLE : View.GONE);
|
||||
|
||||
final RepeatMode repeatMode = playback.getRepeatMode();
|
||||
final boolean repeatChecked = (repeatMode != RepeatMode.None);
|
||||
repeatCb.setText(REPEAT_TO_STRING_ID.get(repeatMode));
|
||||
@ -381,7 +394,6 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
this.albumArtModel.destroy();
|
||||
this.albumArtModel = new AlbumArtModel(title, artist, album, albumArtRetrieved);
|
||||
}
|
||||
|
||||
updateAlbumArt();
|
||||
}
|
||||
}
|
||||
|
@ -764,6 +764,7 @@ public class StreamingPlaybackService implements PlaybackService {
|
||||
.doOnComplete(() -> {
|
||||
if (StreamingPlaybackService.this.params == params) {
|
||||
StreamingPlaybackService.this.context = context;
|
||||
notifyEventListeners();
|
||||
onPlayQueueLoaded();
|
||||
}
|
||||
})
|
||||
|
@ -24,7 +24,7 @@ public class TransportFragment extends Fragment {
|
||||
return new TransportFragment();
|
||||
}
|
||||
|
||||
private View rootView;
|
||||
private View rootView, buffering;
|
||||
private TextView title, playPause;
|
||||
private PlaybackService playback;
|
||||
private OnModelChangedListener modelChangedListener;
|
||||
@ -60,6 +60,7 @@ public class TransportFragment extends Fragment {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
rebindUi();
|
||||
this.playback.connect(this.playbackListener);
|
||||
}
|
||||
|
||||
@ -73,8 +74,10 @@ public class TransportFragment extends Fragment {
|
||||
|
||||
private void bindEventHandlers() {
|
||||
this.title = (TextView) this.rootView.findViewById(R.id.track_title);
|
||||
this.buffering = this.rootView.findViewById(R.id.buffering);
|
||||
|
||||
this.title.setOnClickListener((View view) -> {
|
||||
final View titleBar = this.rootView.findViewById(R.id.title_bar);
|
||||
titleBar.setOnClickListener((View view) -> {
|
||||
if (playback.getPlaybackState() != PlaybackState.Stopped) {
|
||||
final Intent intent = PlayQueueActivity
|
||||
.getStartIntent(getActivity(), playback.getQueuePosition())
|
||||
@ -109,7 +112,10 @@ public class TransportFragment extends Fragment {
|
||||
PlaybackState state = playback.getPlaybackState();
|
||||
|
||||
final boolean playing = (state == PlaybackState.Playing);
|
||||
final boolean buffering = (state == PlaybackState.Buffering);
|
||||
|
||||
this.playPause.setText(playing ? R.string.button_pause : R.string.button_play);
|
||||
this.buffering.setVisibility(buffering ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (state == PlaybackState.Stopped) {
|
||||
title.setTextColor(getActivity().getResources().getColor(R.color.theme_disabled_foreground));
|
||||
@ -117,7 +123,9 @@ public class TransportFragment extends Fragment {
|
||||
}
|
||||
else {
|
||||
title.setTextColor(getActivity().getResources().getColor(R.color.theme_green));
|
||||
title.setText(playback.getTrackString(Metadata.Track.TITLE, "(unknown title)"));
|
||||
|
||||
final String defaultValue = getString(buffering ? R.string.buffering : R.string.unknown_title);
|
||||
title.setText(playback.getTrackString(Metadata.Track.TITLE, defaultValue));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,14 @@
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/with_art_buffering"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<TextView
|
||||
style="@style/LightDropShadow"
|
||||
android:id="@+id/with_art_track_title"
|
||||
@ -103,6 +111,14 @@
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/buffering"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/track_title"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -51,15 +51,42 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp"/>
|
||||
|
||||
<TextView
|
||||
style="@style/TransportPlaying"
|
||||
android:id="@+id/track_title"
|
||||
<FrameLayout
|
||||
android:id="@+id/title_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/theme_disabled_foreground"
|
||||
android:text="@string/transport_not_playing"/>
|
||||
android:background="@drawable/category_button"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/buffering"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:paddingRight="6dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<TextView
|
||||
style="@style/TransportPlaying"
|
||||
android:id="@+id/track_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/theme_disabled_foreground"
|
||||
android:text="@string/transport_not_playing"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
@ -68,4 +68,5 @@
|
||||
<string name="unknown_artist">[unknown artist]</string>
|
||||
<string name="unknown_album">[unknown album]</string>
|
||||
<string name="unknown_title">[unknown title]</string>
|
||||
<string name="buffering">buffering</string>
|
||||
</resources>
|
||||
|
@ -18,13 +18,10 @@
|
||||
|
||||
<style name="TransportPlaying">
|
||||
<item name="android:layout_margin">0dp</item>
|
||||
<item name="android:paddingLeft">4dp</item>
|
||||
<item name="android:paddingRight">4dp</item>
|
||||
<item name="android:paddingTop">6dp</item>
|
||||
<item name="android:paddingBottom">6dp</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:textColor">@color/theme_foreground</item>
|
||||
<item name="android:background">@drawable/category_button</item>
|
||||
<item name="android:clickable">true</item>
|
||||
</style>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user