Commit 65bbb0e0 authored by Max Kellermann's avatar Max Kellermann

Merge tag 'v0.20.17'

release v0.20.17
parents 5147654f c2940a83
...@@ -25,6 +25,15 @@ ver 0.21 (not yet released) ...@@ -25,6 +25,15 @@ ver 0.21 (not yet released)
- sndio: new mixer plugin - sndio: new mixer plugin
* require GCC 5.0 * require GCC 5.0
ver 0.20.17 (2018/02/11)
* output
- alsa: fix crash bug with 8 channels
* mixer
- alsa: fix rounding error at volume 0
* fix real-time and idle scheduling with Musl
* Android
- fix compatibility with Android 4.0
ver 0.20.16 (2018/02/03) ver 0.20.16 (2018/02/03)
* output * output
- pulse: fix crash during auto-detection - pulse: fix crash during auto-detection
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd" package="org.musicpd"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="15" android:versionCode="16"
android:versionName="0.20.16"> android:versionName="0.20.17">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="17"/> <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17"/>
<application android:icon="@drawable/icon" android:label="@string/app_name"> <application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".Main" <activity android:name=".Main"
......
...@@ -46,7 +46,7 @@ class AndroidNdkToolchain: ...@@ -46,7 +46,7 @@ class AndroidNdkToolchain:
self.ndk_arch = 'arm' self.ndk_arch = 'arm'
android_abi = 'armeabi-v7a' android_abi = 'armeabi-v7a'
ndk_platform = 'android-21' ndk_platform = 'android-14'
# select the NDK compiler # select the NDK compiler
gcc_version = '4.9' gcc_version = '4.9'
...@@ -67,7 +67,7 @@ class AndroidNdkToolchain: ...@@ -67,7 +67,7 @@ class AndroidNdkToolchain:
common_flags = '-Os -g' common_flags = '-Os -g'
common_flags += ' -fPIC' common_flags += ' -fPIC'
common_flags += ' -march=armv7-a -mfloat-abi=softfp' common_flags += ' -march=armv7-a -mfpu=vfp -mfloat-abi=softfp'
toolchain_bin = os.path.join(toolchain_path, 'bin') toolchain_bin = os.path.join(toolchain_path, 'bin')
llvm_bin = os.path.join(llvm_path, 'bin') llvm_bin = os.path.join(llvm_path, 'bin')
...@@ -87,7 +87,7 @@ class AndroidNdkToolchain: ...@@ -87,7 +87,7 @@ class AndroidNdkToolchain:
self.cppflags = '--sysroot=' + sysroot + \ self.cppflags = '--sysroot=' + sysroot + \
' -isystem ' + os.path.join(install_prefix, 'include') + \ ' -isystem ' + os.path.join(install_prefix, 'include') + \
' -isystem ' + os.path.join(sysroot, 'usr', 'include', arch) + \ ' -isystem ' + os.path.join(sysroot, 'usr', 'include', arch) + \
' -D__ANDROID_API__=21' ' -D__ANDROID_API__=14'
self.ldflags = '--sysroot=' + sysroot + \ self.ldflags = '--sysroot=' + sysroot + \
' -L' + os.path.join(install_prefix, 'lib') + \ ' -L' + os.path.join(install_prefix, 'lib') + \
' -L' + os.path.join(target_root, 'usr', 'lib') + \ ' -L' + os.path.join(target_root, 'usr', 'lib') + \
......
...@@ -66,6 +66,23 @@ ...@@ -66,6 +66,23 @@
</para> </para>
</section> </section>
<section id="install_android">
<title>Installing on Android</title>
<para>
An experimental Android build is available on <ulink
url="https://play.google.com/store/apps/details?id=org.musicpd">Google
Play</ulink>. After installing and launching it, MPD will
scan the music in your <filename>Music</filename> directory
and you can control it as usual with a MPD client.
</para>
<para>
If you need to tweak the configuration, you can create a file
called <filename>mpd.conf</filename> on the data partition.
</para>
</section>
<section id="install_source"> <section id="install_source">
<title>Compiling from source</title> <title>Compiling from source</title>
...@@ -323,7 +340,9 @@ systemctl start mpd.socket</programlisting> ...@@ -323,7 +340,9 @@ systemctl start mpd.socket</programlisting>
<application>MPD</application> as a user daemon (and not as a <application>MPD</application> as a user daemon (and not as a
system daemon), the configuration is read from system daemon), the configuration is read from
<filename>$XDG_CONFIG_HOME/mpd/mpd.conf</filename> (usually <filename>$XDG_CONFIG_HOME/mpd/mpd.conf</filename> (usually
<filename>~/.config/mpd/mpd.conf</filename>). <filename>~/.config/mpd/mpd.conf</filename>). On Android,
<filename>mpd.conf</filename> will be loaded from the
top-level directory of the data partition.
</para> </para>
<para> <para>
......
import re import re
from os.path import abspath
from build.project import Project from build.project import Project
from build.zlib import ZlibProject from build.zlib import ZlibProject
from build.autotools import AutotoolsProject from build.autotools import AutotoolsProject
...@@ -358,6 +360,8 @@ curl = AutotoolsProject( ...@@ -358,6 +360,8 @@ curl = AutotoolsProject(
'--disable-crypto-auth', '--disable-ntlm-wb', '--disable-tls-srp', '--disable-cookies', '--disable-crypto-auth', '--disable-ntlm-wb', '--disable-tls-srp', '--disable-cookies',
'--without-ssl', '--without-gnutls', '--without-nss', '--without-libssh2', '--without-ssl', '--without-gnutls', '--without-nss', '--without-libssh2',
], ],
patches='src/lib/curl/patches',
) )
boost = BoostProject( boost = BoostProject(
......
...@@ -3,10 +3,12 @@ import re ...@@ -3,10 +3,12 @@ import re
from build.download import download_and_verify from build.download import download_and_verify
from build.tar import untar from build.tar import untar
from build.quilt import push_all
class Project: class Project:
def __init__(self, url, md5, installed, name=None, version=None, def __init__(self, url, md5, installed, name=None, version=None,
base=None, base=None,
patches=None,
edits=None, edits=None,
use_cxx=False): use_cxx=False):
if base is None: if base is None:
...@@ -18,7 +20,7 @@ class Project: ...@@ -18,7 +20,7 @@ class Project:
self.base = base self.base = base
if name is None or version is None: if name is None or version is None:
m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?)$', self.base) m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?[\d.]*)$', self.base)
if name is None: name = m.group(1) if name is None: name = m.group(1)
if version is None: version = m.group(2) if version is None: version = m.group(2)
...@@ -29,6 +31,10 @@ class Project: ...@@ -29,6 +31,10 @@ class Project:
self.md5 = md5 self.md5 = md5
self.installed = installed self.installed = installed
if patches is not None:
srcdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
patches = os.path.join(srcdir, patches)
self.patches = patches
self.edits = edits self.edits = edits
self.use_cxx = use_cxx self.use_cxx = use_cxx
...@@ -51,6 +57,9 @@ class Project: ...@@ -51,6 +57,9 @@ class Project:
parent_path = toolchain.build_path parent_path = toolchain.build_path
path = untar(self.download(toolchain), parent_path, self.base) path = untar(self.download(toolchain), parent_path, self.base)
if self.patches is not None:
push_all(toolchain, path, self.patches)
if self.edits is not None: if self.edits is not None:
for filename, function in self.edits.items(): for filename, function in self.edits.items():
with open(os.path.join(path, filename), 'r+t') as f: with open(os.path.join(path, filename), 'r+t') as f:
......
import subprocess
def run_quilt(toolchain, cwd, patches_path, *args):
env = dict(toolchain.env)
env['QUILT_PATCHES'] = patches_path
subprocess.check_call(['quilt'] + list(args), cwd=cwd, env=env)
def push_all(toolchain, src_path, patches_path):
run_quilt(toolchain, src_path, patches_path, 'push', '-a')
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
#include "util/ChronoUtil.hxx" #include "util/ChronoUtil.hxx"
#include "util/StringStrip.hxx" #include "util/StringStrip.hxx"
#include "util/RuntimeError.hxx" #include "util/RuntimeError.hxx"
#include "util/NumberParser.hxx"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
...@@ -98,7 +99,7 @@ song_load(TextFile &file, const char *uri) ...@@ -98,7 +99,7 @@ song_load(TextFile &file, const char *uri)
if ((type = tag_name_parse(line)) != TAG_NUM_OF_ITEM_TYPES) { if ((type = tag_name_parse(line)) != TAG_NUM_OF_ITEM_TYPES) {
tag.AddItem(type, value); tag.AddItem(type, value);
} else if (strcmp(line, "Time") == 0) { } else if (strcmp(line, "Time") == 0) {
tag.SetDuration(SignedSongTime::FromS(atof(value))); tag.SetDuration(SignedSongTime::FromS(ParseDouble(value)));
} else if (strcmp(line, "Playlist") == 0) { } else if (strcmp(line, "Playlist") == 0) {
tag.SetHasPlaylist(strcmp(value, "yes") == 0); tag.SetHasPlaylist(strcmp(value, "yes") == 0);
} else if (strcmp(line, SONG_MTIME) == 0) { } else if (strcmp(line, SONG_MTIME) == 0) {
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
#include "Idle.hxx" #include "Idle.hxx"
#include "Log.hxx" #include "Log.hxx"
#include "thread/Thread.hxx" #include "thread/Thread.hxx"
#include "thread/Name.hxx"
#include "thread/Util.hxx" #include "thread/Util.hxx"
#ifndef NDEBUG #ifndef NDEBUG
...@@ -111,6 +112,8 @@ UpdateService::Task() ...@@ -111,6 +112,8 @@ UpdateService::Task()
{ {
assert(walk != nullptr); assert(walk != nullptr);
SetThreadName("update");
if (!next.path_utf8.empty()) if (!next.path_utf8.empty())
FormatDebug(update_domain, "starting: %s", FormatDebug(update_domain, "starting: %s",
next.path_utf8.c_str()); next.path_utf8.c_str());
......
...@@ -51,7 +51,8 @@ public: ...@@ -51,7 +51,8 @@ public:
void Set(const AudioFormat &_out_audio_format); void Set(const AudioFormat &_out_audio_format);
void Reset() noexcept override { void Reset() noexcept override {
state.Reset(); if (IsActive())
state.Reset();
} }
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override; ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
......
...@@ -65,10 +65,13 @@ OpenFileInputStream(Path path, ...@@ -65,10 +65,13 @@ OpenFileInputStream(Path path,
throw FormatRuntimeError("Not a regular file: %s", throw FormatRuntimeError("Not a regular file: %s",
path.c_str()); path.c_str());
#if !defined(__BIONIC__) || __ANDROID_API__ >= 21
/* posix_fadvise() requires Android API 21 */
#ifdef POSIX_FADV_SEQUENTIAL #ifdef POSIX_FADV_SEQUENTIAL
posix_fadvise(reader.GetFD().Get(), (off_t)0, info.GetSize(), posix_fadvise(reader.GetFD().Get(), (off_t)0, info.GetSize(),
POSIX_FADV_SEQUENTIAL); POSIX_FADV_SEQUENTIAL);
#endif #endif
#endif
return std::make_unique<FileInputStream>(path.ToUTF8().c_str(), return std::make_unique<FileInputStream>(path.ToUTF8().c_str(),
std::move(reader), info.GetSize(), std::move(reader), info.GetSize(),
......
Index: curl-7.58.0/lib/url.c
===================================================================
--- curl-7.58.0.orig/lib/url.c
+++ curl-7.58.0/lib/url.c
@@ -3503,6 +3503,7 @@ static CURLcode override_login(struct Cu
}
conn->bits.netrc = FALSE;
+#ifndef __BIONIC__
if(data->set.use_netrc != CURL_NETRC_IGNORED) {
int ret = Curl_parsenetrc(conn->host.name,
userp, passwdp,
@@ -3524,6 +3525,7 @@ static CURLcode override_login(struct Cu
conn->bits.user_passwd = TRUE; /* enable user+password */
}
}
+#endif
return CURLE_OK;
}
Index: curl-7.58.0/Makefile.in
===================================================================
--- curl-7.58.0.orig/Makefile.in
+++ curl-7.58.0/Makefile.in
@@ -641,8 +641,8 @@ CLEANFILES = $(VC6_LIBDSP) $(VC6_SRCDSP)
$(VC14_LIBVCXPROJ) $(VC14_SRCVCXPROJ) $(VC15_LIBVCXPROJ) $(VC15_SRCVCXPROJ)
bin_SCRIPTS = curl-config
-SUBDIRS = lib src
-DIST_SUBDIRS = $(SUBDIRS) tests packages scripts include docs
+SUBDIRS = lib
+DIST_SUBDIRS = $(SUBDIRS) include
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = libcurl.pc
LIB_VAUTH_CFILES = vauth/vauth.c vauth/cleartext.c vauth/cram.c \
only_lib.patch
no_netrc.patch
...@@ -139,6 +139,13 @@ static int set_normalized_volume(snd_mixer_elem_t *elem, ...@@ -139,6 +139,13 @@ static int set_normalized_volume(snd_mixer_elem_t *elem,
return set_raw[ctl_dir](elem, value); return set_raw[ctl_dir](elem, value);
} }
/* two special cases to avoid rounding errors at 0% and
100% */
if (volume <= 0)
return set_dB[ctl_dir](elem, min, dir);
else if (volume >= 100)
return set_dB[ctl_dir](elem, max, dir);
if (use_linear_dB_scale(min, max)) { if (use_linear_dB_scale(min, max)) {
value = lrint_dir(volume * (max - min), dir) + min; value = lrint_dir(volume * (max - min), dir) + min;
return set_dB[ctl_dir](elem, value, dir); return set_dB[ctl_dir](elem, value, dir);
......
...@@ -88,7 +88,7 @@ static inline ConstBuffer<V> ...@@ -88,7 +88,7 @@ static inline ConstBuffer<V>
ToAlsaChannelOrder71(PcmBuffer &buffer, ConstBuffer<V> src) noexcept ToAlsaChannelOrder71(PcmBuffer &buffer, ConstBuffer<V> src) noexcept
{ {
auto dest = buffer.GetT<V>(src.size); auto dest = buffer.GetT<V>(src.size);
ToAlsaChannelOrder71(dest, src.data, src.size / 6); ToAlsaChannelOrder71(dest, src.data, src.size / 8);
return { dest, src.size }; return { dest, src.size };
} }
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
#include "ArgParser.hxx" #include "ArgParser.hxx"
#include "Ack.hxx" #include "Ack.hxx"
#include "Chrono.hxx" #include "Chrono.hxx"
#include "util/NumberParser.hxx"
#include <stdlib.h> #include <stdlib.h>
...@@ -151,7 +152,7 @@ float ...@@ -151,7 +152,7 @@ float
ParseCommandArgFloat(const char *s) ParseCommandArgFloat(const char *s)
{ {
char *endptr; char *endptr;
auto value = strtof(s, &endptr); auto value = ParseFloat(s, &endptr);
if (endptr == s || *endptr != 0) if (endptr == s || *endptr != 0)
throw FormatProtocolError(ACK_ERROR_ARG, throw FormatProtocolError(ACK_ERROR_ARG,
"Float expected: %s", s); "Float expected: %s", s);
......
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
#include "util/CharUtil.hxx" #include "util/CharUtil.hxx"
#include "util/StringAPI.hxx" #include "util/StringAPI.hxx"
#include "util/StringCompare.hxx" #include "util/StringCompare.hxx"
#include "util/NumberParser.hxx"
#include "Log.hxx" #include "Log.hxx"
#include <string.h> #include <string.h>
...@@ -148,7 +149,7 @@ playlist_state_restore(const char *line, TextFile &file, ...@@ -148,7 +149,7 @@ playlist_state_restore(const char *line, TextFile &file,
while ((line = file.ReadLine()) != nullptr) { while ((line = file.ReadLine()) != nullptr) {
const char *p; const char *p;
if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_TIME))) { if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_TIME))) {
seek_time = SongTime::FromS(atof(p)); seek_time = SongTime::FromS(ParseDouble(p));
} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_REPEAT))) { } else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_REPEAT))) {
playlist.SetRepeat(pc, StringIsEqual(p, "1")); playlist.SetRepeat(pc, StringIsEqual(p, "1"));
} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_SINGLE))) { } else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_SINGLE))) {
...@@ -158,12 +159,12 @@ playlist_state_restore(const char *line, TextFile &file, ...@@ -158,12 +159,12 @@ playlist_state_restore(const char *line, TextFile &file,
} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_CROSSFADE))) { } else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_CROSSFADE))) {
pc.SetCrossFade(atoi(p)); pc.SetCrossFade(atoi(p));
} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_MIXRAMPDB))) { } else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_MIXRAMPDB))) {
pc.SetMixRampDb(atof(p)); pc.SetMixRampDb(ParseFloat(p));
} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_MIXRAMPDELAY))) { } else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_MIXRAMPDELAY))) {
/* this check discards "nan" which was used /* this check discards "nan" which was used
prior to MPD 0.18 */ prior to MPD 0.18 */
if (IsDigitASCII(*p)) if (IsDigitASCII(*p))
pc.SetMixRampDelay(atof(p)); pc.SetMixRampDelay(ParseFloat(p));
} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_RANDOM))) { } else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_RANDOM))) {
random_mode = StringIsEqual(p, "1"); random_mode = StringIsEqual(p, "1");
} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_CURRENT))) { } else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_CURRENT))) {
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
#include "VorbisComment.hxx" #include "VorbisComment.hxx"
#include "ReplayGainInfo.hxx" #include "ReplayGainInfo.hxx"
#include "util/ASCII.hxx" #include "util/ASCII.hxx"
#include "util/NumberParser.hxx"
#include <assert.h> #include <assert.h>
#include <stdlib.h> #include <stdlib.h>
...@@ -33,16 +34,16 @@ ParseReplayGainTagTemplate(ReplayGainInfo &info, const T t) ...@@ -33,16 +34,16 @@ ParseReplayGainTagTemplate(ReplayGainInfo &info, const T t)
const char *value; const char *value;
if ((value = t["replaygain_track_gain"]) != nullptr) { if ((value = t["replaygain_track_gain"]) != nullptr) {
info.track.gain = atof(value); info.track.gain = ParseFloat(value);
return true; return true;
} else if ((value = t["replaygain_album_gain"]) != nullptr) { } else if ((value = t["replaygain_album_gain"]) != nullptr) {
info.album.gain = atof(value); info.album.gain = ParseFloat(value);
return true; return true;
} else if ((value = t["replaygain_track_peak"]) != nullptr) { } else if ((value = t["replaygain_track_peak"]) != nullptr) {
info.track.peak = atof(value); info.track.peak = ParseFloat(value);
return true; return true;
} else if ((value = t["replaygain_album_peak"]) != nullptr) { } else if ((value = t["replaygain_album_peak"]) != nullptr) {
info.album.peak = atof(value); info.album.peak = ParseFloat(value);
return true; return true;
} else } else
return false; return false;
......
...@@ -38,10 +38,12 @@ ...@@ -38,10 +38,12 @@
#include <windows.h> #include <windows.h>
#endif #endif
#if defined(__linux__) && !defined(ANDROID) #ifdef __linux__
#ifndef ANDROID
static int static int
ioprio_set(int which, int who, int ioprio) noexcept linux_ioprio_set(int which, int who, int ioprio) noexcept
{ {
return syscall(__NR_ioprio_set, which, who, ioprio); return syscall(__NR_ioprio_set, which, who, ioprio);
} }
...@@ -55,7 +57,21 @@ ioprio_set_idle() noexcept ...@@ -55,7 +57,21 @@ ioprio_set_idle() noexcept
static constexpr int _IOPRIO_IDLE = static constexpr int _IOPRIO_IDLE =
(_IOPRIO_CLASS_IDLE << _IOPRIO_CLASS_SHIFT) | 7; (_IOPRIO_CLASS_IDLE << _IOPRIO_CLASS_SHIFT) | 7;
ioprio_set(_IOPRIO_WHO_PROCESS, 0, _IOPRIO_IDLE); linux_ioprio_set(_IOPRIO_WHO_PROCESS, 0, _IOPRIO_IDLE);
}
#endif /* !ANDROID */
/**
* Wrapper for the "sched_setscheduler" system call. We don't use the
* one from the C library because Musl has an intentionally broken
* implementation.
*/
static int
linux_sched_setscheduler(pid_t pid, int sched,
const struct sched_param *param) noexcept
{
return syscall(__NR_sched_setscheduler, pid, sched, param);
} }
#endif #endif
...@@ -66,7 +82,7 @@ SetThreadIdlePriority() noexcept ...@@ -66,7 +82,7 @@ SetThreadIdlePriority() noexcept
#ifdef __linux__ #ifdef __linux__
#ifdef SCHED_IDLE #ifdef SCHED_IDLE
static struct sched_param sched_param; static struct sched_param sched_param;
sched_setscheduler(0, SCHED_IDLE, &sched_param); linux_sched_setscheduler(0, SCHED_IDLE, &sched_param);
#endif #endif
#ifndef ANDROID #ifndef ANDROID
...@@ -92,7 +108,7 @@ SetThreadRealtime() ...@@ -92,7 +108,7 @@ SetThreadRealtime()
policy |= SCHED_RESET_ON_FORK; policy |= SCHED_RESET_ON_FORK;
#endif #endif
if (sched_setscheduler(0, policy, &sched_param) < 0) if (linux_sched_setscheduler(0, policy, &sched_param) < 0)
throw MakeErrno("sched_setscheduler failed"); throw MakeErrno("sched_setscheduler failed");
#endif // __linux__ #endif // __linux__
}; };
...@@ -78,7 +78,12 @@ ParseDouble(const char *p, char **endptr=nullptr) ...@@ -78,7 +78,12 @@ ParseDouble(const char *p, char **endptr=nullptr)
static inline float static inline float
ParseFloat(const char *p, char **endptr=nullptr) ParseFloat(const char *p, char **endptr=nullptr)
{ {
#if defined(__BIONIC__) && __ANDROID_API__ < 21
/* strtof() requires API level 21 */
return (float)ParseDouble(p, endptr); return (float)ParseDouble(p, endptr);
#else
return strtof(p, endptr);
#endif
} }
#endif #endif
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment