diff --git a/.travis.yml b/.travis.yml
index d57057db6..38bf7a331 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -100,6 +100,22 @@ jobs:
           packages:
             - ccache
             - meson
+            - icu4c
+            - ffmpeg
+            - libnfs
+            - yajl
+            - libupnp
+            - libid3tag
+            - chromaprint
+            - libsamplerate
+            - libsoxr
+            - libzzip
+            - flac
+            - opus
+            - libvorbis
+            - faad2
+            - wavpack
+            - libmpdclient
           update: true
       env:
         - MATRIX_EVAL="export PATH=/usr/local/opt/ccache/libexec:$PATH HOMEBREW_NO_ANALYTICS=1"
diff --git a/NEWS b/NEWS
index f4cb15cbd..5dbb3c53d 100644
--- a/NEWS
+++ b/NEWS
@@ -35,6 +35,21 @@ ver 0.22 (not yet released)
 * switch to C++17
   - GCC 7 or clang 4 (or newer) recommended
 
+ver 0.21.22 (2020/04/02)
+* database
+  - simple: optimize startup
+* input
+  - curl: fix streaming errors on Android
+* playlist
+  - rss: support MIME type application/xml
+* mixer
+  - android: new mixer plugin for "sles" output
+* Android
+  - TV support
+* Windows
+  - fix time zone offset check
+* fix build failures with uClibc-ng
+
 ver 0.21.21 (2020/03/19)
 * configuration
   - fix bug in "metadata_to_use" setting
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index 83286b0b7..4dcdba4fd 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -2,18 +2,25 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="org.musicpd"
           android:installLocation="auto"
-          android:versionCode="44"
-          android:versionName="0.21.21">
+          android:versionCode="45"
+          android:versionName="0.21.22">
 
   <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28"/>
 
+  <uses-feature android:name="android.software.leanback"
+                android:required="false" />
+  <uses-feature android:name="android.hardware.touchscreen"
+                android:required="false" />
+
   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
   <uses-permission android:name="android.permission.WAKE_LOCK"/>
   <uses-permission android:name="android.permission.INTERNET"/>
   <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 
   <application android:allowBackup="true"
                android:icon="@drawable/icon"
+               android:banner="@drawable/icon"
                android:label="@string/app_name">
     <activity android:name=".Settings"
               android:label="@string/app_name">
@@ -22,6 +29,14 @@
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
     </activity>
+    <activity android:name=".Settings"
+              android:label="@string/app_name" >
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+        <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+      </intent-filter>
+    </activity>
+
     <receiver android:name=".Receiver">
       <intent-filter>
         <action android:name="android.intent.action.BOOT_COMPLETED" />
diff --git a/android/src/Main.java b/android/src/Main.java
index 57abee56c..b0c63136a 100644
--- a/android/src/Main.java
+++ b/android/src/Main.java
@@ -21,6 +21,7 @@ package org.musicpd;
 
 import android.annotation.TargetApi;
 import android.app.Notification;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
 import android.content.ComponentName;
@@ -35,6 +36,9 @@ import android.os.RemoteException;
 import android.util.Log;
 import android.widget.RemoteViews;
 
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
 public class Main extends Service implements Runnable {
 	private static final String TAG = "Main";
 	private static final String REMOTE_ERROR = "MPD process was killed";
@@ -156,11 +160,36 @@ public class Main extends Service implements Runnable {
 		sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
 	}
 
+	private Notification.Builder createNotificationBuilderWithChannel() {
+		final NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
+		if (notificationManager == null)
+			return null;
+
+		final String id = "org.musicpd";
+		final String name = "MPD service";
+		final int importance = 3; /* NotificationManager.IMPORTANCE_DEFAULT */
+
+		try {
+			Class<?> ncClass = Class.forName("android.app.NotificationChannel");
+			Constructor<?> ncCtor = ncClass.getConstructor(String.class, CharSequence.class, int.class);
+			Object nc = ncCtor.newInstance(id, name, importance);
+
+			Method nmCreateNotificationChannelMethod =
+				NotificationManager.class.getMethod("createNotificationChannel", ncClass);
+			nmCreateNotificationChannelMethod.invoke(notificationManager, nc);
+
+			Constructor nbCtor = Notification.Builder.class.getConstructor(Context.class, String.class);
+			return (Notification.Builder) nbCtor.newInstance(this, id);
+		} catch (Exception e)
+		{
+			Log.e(TAG, "error creating the NotificationChannel", e);
+			return null;
+		}
+	}
+
 	private void start() {
 		if (mThread != null)
 			return;
-		mThread = new Thread(this);
-		mThread.start();
 
 		final Intent mainIntent = new Intent(this, Settings.class);
 		mainIntent.setAction("android.intent.action.MAIN");
@@ -168,13 +197,25 @@ public class Main extends Service implements Runnable {
 		final PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
 				mainIntent, PendingIntent.FLAG_CANCEL_CURRENT);
 
-		Notification notification = new Notification.Builder(this)
-			.setContentTitle(getText(R.string.notification_title_mpd_running))
+		Notification.Builder nBuilder;
+		if (Build.VERSION.SDK_INT >= 26 /* Build.VERSION_CODES.O */)
+		{
+			nBuilder = createNotificationBuilderWithChannel();
+			if (nBuilder == null)
+				return;
+		}
+		else
+			nBuilder = new Notification.Builder(this);
+
+		Notification notification = nBuilder.setContentTitle(getText(R.string.notification_title_mpd_running))
 			.setContentText(getText(R.string.notification_text_mpd_running))
 			.setSmallIcon(R.drawable.notification_icon)
 			.setContentIntent(contentIntent)
 			.build();
 
+		mThread = new Thread(this);
+		mThread.start();
+
 		startForeground(R.string.notification_title_mpd_running, notification);
 		startService(new Intent(this, Main.class));
 	}
diff --git a/android/src/Settings.java b/android/src/Settings.java
index af3008be5..4650bbce6 100644
--- a/android/src/Settings.java
+++ b/android/src/Settings.java
@@ -105,12 +105,13 @@ public class Settings extends Activity {
 				else
 					mRunButton.setChecked(false);
 				mFirstRun = true;
+				mTextStatus.setText("");
 				break;
 			case MSG_STARTED:
 				Log.d(TAG, "onStarted");
 				mRunButton.setChecked(true);
 				mFirstRun = true;
-				mTextStatus.setText("CAUTION: this version is EXPERIMENTAL!"); // XXX
+				mTextStatus.setText("MPD service started");
 				break;
 			case MSG_LOG:
 				if (mLogListArray.size() > MAX_LOGS)
diff --git a/meson.build b/meson.build
index a9116f90a..1aef557f9 100644
--- a/meson.build
+++ b/meson.build
@@ -311,6 +311,7 @@ if not is_android
 else
   sources += [
     'src/android/Context.cxx',
+    'src/android/AudioManager.cxx',
     'src/android/Environment.cxx',
     'src/android/LogListener.cxx',
   ]
@@ -332,6 +333,7 @@ subdir('src/util')
 subdir('src/time')
 subdir('src/system')
 subdir('src/thread')
+subdir('src/net')
 subdir('src/event')
 
 subdir('src/lib/dbus')
@@ -359,7 +361,6 @@ subdir('src/lib/crypto')
 
 subdir('src/fs')
 subdir('src/config')
-subdir('src/net')
 subdir('src/tag')
 subdir('src/pcm')
 subdir('src/neighbor')
diff --git a/python/build/libs.py b/python/build/libs.py
index 7c317d46a..af018ed8f 100644
--- a/python/build/libs.py
+++ b/python/build/libs.py
@@ -9,8 +9,8 @@ from build.ffmpeg import FfmpegProject
 from build.boost import BoostProject
 
 libmpdclient = MesonProject(
-    'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.17.tar.xz',
-    'ee9b8f1c7e95b65c8f18a354daf7b16bfcd455fc52a0f3b5abe402316bce3559',
+    'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.18.tar.xz',
+    '4cb01e1f567e0169aca94875fb6e1200e7f5ce35b63a4df768ec1591fb1081fa',
     'lib/libmpdclient.a',
 )
 
@@ -341,8 +341,8 @@ ffmpeg = FfmpegProject(
 )
 
 curl = AutotoolsProject(
-    'http://curl.haxx.se/download/curl-7.68.0.tar.xz',
-    'b724240722276a27f6e770b952121a3afd097129d8c9fe18e6272dc34192035a',
+    'http://curl.haxx.se/download/curl-7.69.1.tar.xz',
+    '03c7d5e6697f7b7e40ada1b2256e565a555657398e6c1fcfa4cb251ccd819d4f',
     'lib/libcurl.a',
     [
         '--disable-shared', '--enable-static',
diff --git a/src/CommandLine.cxx b/src/CommandLine.cxx
index e6682c20f..213867168 100644
--- a/src/CommandLine.cxx
+++ b/src/CommandLine.cxx
@@ -33,6 +33,7 @@
 #include "playlist/PlaylistRegistry.hxx"
 #include "playlist/PlaylistPlugin.hxx"
 #include "fs/AllocatedPath.hxx"
+#include "fs/NarrowPath.hxx"
 #include "fs/Traits.hxx"
 #include "fs/FileSystem.hxx"
 #include "fs/StandardDirectory.hxx"
@@ -378,17 +379,7 @@ ParseCommandLine(int argc, char **argv, struct options &options,
 
 	if (config_file != nullptr) {
 		/* use specified configuration file */
-#ifdef _UNICODE
-		wchar_t buffer[MAX_PATH];
-		auto result = MultiByteToWideChar(CP_ACP, 0, config_file, -1,
-						  buffer, std::size(buffer));
-		if (result <= 0)
-			throw MakeLastError("MultiByteToWideChar() failed");
-
-		ReadConfigFile(config, Path::FromFS(buffer));
-#else
-		ReadConfigFile(config, Path::FromFS(config_file));
-#endif
+		ReadConfigFile(config, FromNarrowPath(config_file));
 		return;
 	}
 
diff --git a/src/android/AudioManager.cxx b/src/android/AudioManager.cxx
new file mode 100644
index 000000000..5e3062250
--- /dev/null
+++ b/src/android/AudioManager.cxx
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2003-2020 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "AudioManager.hxx"
+#include "java/Class.hxx"
+#include "java/Exception.hxx"
+#include "java/File.hxx"
+
+#define STREAM_MUSIC 3
+
+AudioManager::AudioManager(JNIEnv *env, jobject obj) noexcept
+	: Java::GlobalObject(env, obj)
+{
+	Java::Class cls(env, env->GetObjectClass(Get()));
+	jmethodID method = env->GetMethodID(cls, "getStreamMaxVolume", "(I)I");
+	assert(method);
+	maxVolume = env->CallIntMethod(Get(), method, STREAM_MUSIC);
+
+	getStreamVolumeMethod = env->GetMethodID(cls, "getStreamVolume", "(I)I");
+	assert(getStreamVolumeMethod);
+
+	setStreamVolumeMethod = env->GetMethodID(cls, "setStreamVolume", "(III)V");
+	assert(setStreamVolumeMethod);
+}
+
+int
+AudioManager::GetVolume(JNIEnv *env)
+{
+	if (maxVolume == 0)
+		return 0;
+	return env->CallIntMethod(Get(), getStreamVolumeMethod, STREAM_MUSIC);
+}
+
+void
+AudioManager::SetVolume(JNIEnv *env, int volume)
+{
+	if (maxVolume == 0)
+		return;
+	env->CallVoidMethod(Get(), setStreamVolumeMethod, STREAM_MUSIC, volume, 0);
+}
diff --git a/src/android/AudioManager.hxx b/src/android/AudioManager.hxx
new file mode 100644
index 000000000..d4de3deb3
--- /dev/null
+++ b/src/android/AudioManager.hxx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2003-2020 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_ANDROID_AUDIO_MANAGER_HXX
+#define MPD_ANDROID_AUDIO_MANAGER_HXX
+
+#include "java/Object.hxx"
+
+class AudioManager : public Java::GlobalObject {
+	int maxVolume;
+	jmethodID getStreamVolumeMethod;
+	jmethodID setStreamVolumeMethod;
+
+public:
+	AudioManager(JNIEnv *env, jobject obj) noexcept;
+
+	AudioManager(std::nullptr_t) noexcept { maxVolume = 0; }
+
+	~AudioManager() noexcept {}
+
+	int GetMaxVolume() { return maxVolume; }
+	int GetVolume(JNIEnv *env);
+	void SetVolume(JNIEnv *env, int);
+};
+
+#endif
diff --git a/src/android/Context.cxx b/src/android/Context.cxx
index b4248fa2b..c461c4a1b 100644
--- a/src/android/Context.cxx
+++ b/src/android/Context.cxx
@@ -21,8 +21,11 @@
 #include "java/Class.hxx"
 #include "java/Exception.hxx"
 #include "java/File.hxx"
+#include "java/String.hxx"
 #include "fs/AllocatedPath.hxx"
 
+#include "AudioManager.hxx"
+
 AllocatedPath
 Context::GetCacheDir(JNIEnv *env) const noexcept
 {
@@ -39,3 +42,21 @@ Context::GetCacheDir(JNIEnv *env) const noexcept
 
 	return Java::File::ToAbsolutePath(env, file);
 }
+
+AudioManager *
+Context::GetAudioManager(JNIEnv *env) noexcept
+{
+	assert(env != nullptr);
+
+	Java::Class cls(env, env->GetObjectClass(Get()));
+	jmethodID method = env->GetMethodID(cls, "getSystemService",
+					    "(Ljava/lang/String;)Ljava/lang/Object;");
+	assert(method);
+
+	Java::String name(env, "audio");
+	jobject am = env->CallObjectMethod(Get(), method, name.Get());
+	if (Java::DiscardException(env) || am == nullptr)
+		return nullptr;
+
+    return new AudioManager(env, am);
+}
diff --git a/src/android/Context.hxx b/src/android/Context.hxx
index 64687cada..a8ae5070f 100644
--- a/src/android/Context.hxx
+++ b/src/android/Context.hxx
@@ -23,6 +23,7 @@
 #include "java/Object.hxx"
 
 class AllocatedPath;
+class AudioManager;
 
 class Context : public Java::GlobalObject {
 public:
@@ -31,6 +32,9 @@ public:
 
 	gcc_pure
 	AllocatedPath GetCacheDir(JNIEnv *env) const noexcept;
+
+	gcc_pure
+	AudioManager *GetAudioManager(JNIEnv *env) noexcept;
 };
 
 #endif
diff --git a/src/db/plugins/simple/Directory.cxx b/src/db/plugins/simple/Directory.cxx
index 72d169086..0c0811540 100644
--- a/src/db/plugins/simple/Directory.cxx
+++ b/src/db/plugins/simple/Directory.cxx
@@ -32,6 +32,7 @@
 #include "fs/Traits.hxx"
 #include "util/Alloc.hxx"
 #include "util/DeleteDisposer.hxx"
+#include "util/StringCompare.hxx"
 
 #include <cassert>
 
@@ -70,7 +71,15 @@ Directory::GetName() const noexcept
 {
 	assert(!IsRoot());
 
-	return PathTraitsUTF8::GetBase(path.c_str());
+	if (parent->IsRoot())
+		return path.c_str();
+
+	assert(StringAfterPrefix(path.c_str(), parent->path.c_str()) != nullptr);
+	assert(*StringAfterPrefix(path.c_str(), parent->path.c_str()) == PathTraitsUTF8::SEPARATOR);
+
+	/* strip the parent directory path and the slash separator
+	   from this directory's path, and the base name remains */
+	return path.c_str() + parent->path.length() + 1;
 }
 
 Directory *
diff --git a/src/decoder/plugins/FfmpegDecoderPlugin.cxx b/src/decoder/plugins/FfmpegDecoderPlugin.cxx
index 9bedd3f7e..971241826 100644
--- a/src/decoder/plugins/FfmpegDecoderPlugin.cxx
+++ b/src/decoder/plugins/FfmpegDecoderPlugin.cxx
@@ -287,7 +287,7 @@ FfmpegReceiveFrames(DecoderClient &client, InputStream &is,
  */
 static DecoderCommand
 ffmpeg_send_packet(DecoderClient &client, InputStream &is,
-		   AVPacket &&packet,
+		   const AVPacket &packet,
 		   AVCodecContext &codec_context,
 		   const AVStream &stream,
 		   AVFrame &frame,
@@ -340,24 +340,6 @@ ffmpeg_send_packet(DecoderClient &client, InputStream &is,
 	return cmd;
 }
 
-static DecoderCommand
-ffmpeg_send_packet(DecoderClient &client, InputStream &is,
-		   const AVPacket &packet,
-		   AVCodecContext &codec_context,
-		   const AVStream &stream,
-		   AVFrame &frame,
-		   uint64_t min_frame, size_t pcm_frame_size,
-		   FfmpegBuffer &buffer)
-{
-	return ffmpeg_send_packet(client, is,
-				  /* copy the AVPacket, because FFmpeg
-				     < 3.0 requires this */
-				  AVPacket(packet),
-				  codec_context, stream,
-				  frame, min_frame, pcm_frame_size,
-				  buffer);
-}
-
 gcc_const
 static SampleFormat
 ffmpeg_sample_format(enum AVSampleFormat sample_fmt) noexcept
diff --git a/src/event/meson.build b/src/event/meson.build
index 268734f5a..6877bc37a 100644
--- a/src/event/meson.build
+++ b/src/event/meson.build
@@ -26,6 +26,7 @@ event_dep = declare_dependency(
   link_with: event,
   dependencies: [
     thread_dep,
+    net_dep,
     system_dep,
     boost_dep,
   ],
diff --git a/src/fs/NarrowPath.cxx b/src/fs/NarrowPath.cxx
new file mode 100644
index 000000000..e2597f83f
--- /dev/null
+++ b/src/fs/NarrowPath.cxx
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "NarrowPath.hxx"
+
+#ifdef _UNICODE
+
+#include "lib/icu/Win32.hxx"
+#include "system/Error.hxx"
+
+#include <windows.h>
+
+NarrowPath::NarrowPath(Path _path) noexcept
+	:value(WideCharToMultiByte(CP_ACP, _path.c_str()))
+{
+	if (value.IsNull())
+		/* fall back to empty string */
+		value = Value::Empty();
+}
+
+static AllocatedPath
+AcpToAllocatedPath(const char *s)
+{
+	wchar_t buffer[MAX_PATH];
+	auto result = MultiByteToWideChar(CP_ACP, 0, s, -1,
+					  buffer, std::size(buffer));
+	if (result <= 0)
+		throw MakeLastError("MultiByteToWideChar() failed");
+
+	return AllocatedPath::FromFS(buffer);
+}
+
+FromNarrowPath::FromNarrowPath(const char *s)
+	:value(AcpToAllocatedPath(s))
+{
+}
+
+#endif /* _UNICODE */
diff --git a/src/fs/NarrowPath.hxx b/src/fs/NarrowPath.hxx
index 9ac5d926b..c0e15ea23 100644
--- a/src/fs/NarrowPath.hxx
+++ b/src/fs/NarrowPath.hxx
@@ -23,9 +23,8 @@
 #include "Path.hxx"
 
 #ifdef _UNICODE
-#include "lib/icu/Win32.hxx"
+#include "AllocatedPath.hxx"
 #include "util/AllocatedString.hxx"
-#include <windows.h>
 #else
 #include "util/StringPointer.hxx"
 #endif
@@ -47,12 +46,7 @@ class NarrowPath {
 
 public:
 #ifdef _UNICODE
-	explicit NarrowPath(Path _path)
-		:value(WideCharToMultiByte(CP_ACP, _path.c_str())) {
-		if (value.IsNull())
-			/* fall back to empty string */
-			value = Value::Empty();
-	}
+	explicit NarrowPath(Path _path) noexcept;
 #else
 	explicit NarrowPath(Path _path):value(_path.c_str()) {}
 #endif
@@ -66,4 +60,38 @@ public:
 	}
 };
 
+/**
+ * A path name converted from a "narrow" string.  This is used to
+ * import an existing narrow string to a #Path.
+ */
+class FromNarrowPath {
+#ifdef _UNICODE
+	using Value = AllocatedPath;
+#else
+	using Value = Path;
+#endif
+
+	Value value{nullptr};
+
+public:
+	FromNarrowPath() = default;
+
+#ifdef _UNICODE
+	/**
+	 * Throws on error.
+	 */
+	FromNarrowPath(const char *s);
+#else
+	constexpr FromNarrowPath(const char *s) noexcept
+		:value(Value::FromFS(s)) {}
+#endif
+
+#ifndef _UNICODE
+	constexpr
+#endif
+	operator Path() const noexcept {
+		return value;
+	}
+};
+
 #endif
diff --git a/src/fs/io/BufferedReader.hxx b/src/fs/io/BufferedReader.hxx
index 819b74e6d..536d8f482 100644
--- a/src/fs/io/BufferedReader.hxx
+++ b/src/fs/io/BufferedReader.hxx
@@ -50,7 +50,7 @@ class BufferedReader {
 
 public:
 	explicit BufferedReader(Reader &_reader) noexcept
-		:reader(_reader), buffer(4096) {}
+		:reader(_reader), buffer(16384) {}
 
 	/**
 	 * Reset the internal state.  Should be called after rewinding
diff --git a/src/fs/io/GunzipReader.hxx b/src/fs/io/GunzipReader.hxx
index 46a00540d..ce2aee3f5 100644
--- a/src/fs/io/GunzipReader.hxx
+++ b/src/fs/io/GunzipReader.hxx
@@ -45,7 +45,7 @@ class GunzipReader final : public Reader {
 
 	z_stream z;
 
-	StaticFifoBuffer<Bytef, 4096> buffer;
+	StaticFifoBuffer<Bytef, 65536> buffer;
 
 public:
 	/**
diff --git a/src/fs/io/GzipOutputStream.cxx b/src/fs/io/GzipOutputStream.cxx
index 67f97a278..051582ae3 100644
--- a/src/fs/io/GzipOutputStream.cxx
+++ b/src/fs/io/GzipOutputStream.cxx
@@ -62,7 +62,7 @@ GzipOutputStream::Flush()
 	z.avail_in = 0;
 
 	while (true) {
-		Bytef output[4096];
+		Bytef output[16384];
 		z.next_out = output;
 		z.avail_out = sizeof(output);
 
@@ -87,7 +87,7 @@ GzipOutputStream::Write(const void *_data, size_t size)
 	z.avail_in = size;
 
 	while (z.avail_in > 0) {
-		Bytef output[4096];
+		Bytef output[16384];
 		z.next_out = output;
 		z.avail_out = sizeof(output);
 
diff --git a/src/fs/meson.build b/src/fs/meson.build
index daf14a201..980ca6138 100644
--- a/src/fs/meson.build
+++ b/src/fs/meson.build
@@ -6,6 +6,7 @@ fs_sources = [
   'Path.cxx',
   'Path2.cxx',
   'AllocatedPath.cxx',
+  'NarrowPath.cxx',
   'FileSystem.cxx',
   'List.cxx',
   'StandardDirectory.cxx',
diff --git a/src/lib/curl/Request.cxx b/src/lib/curl/Request.cxx
index eb3afbff1..5051d4d16 100644
--- a/src/lib/curl/Request.cxx
+++ b/src/lib/curl/Request.cxx
@@ -56,7 +56,9 @@ CurlRequest::CurlRequest(CurlGlobal &_global,
 	easy.SetUserAgent("Music Player Daemon " VERSION);
 	easy.SetHeaderFunction(_HeaderFunction, this);
 	easy.SetWriteFunction(WriteFunction, this);
+#ifndef ANDROID
 	easy.SetOption(CURLOPT_NETRC, 1L);
+#endif
 	easy.SetErrorBuffer(error_buffer);
 	easy.SetNoProgress();
 	easy.SetNoSignal();
diff --git a/src/mixer/MixerList.hxx b/src/mixer/MixerList.hxx
index d64d5d34b..7f68d84f5 100644
--- a/src/mixer/MixerList.hxx
+++ b/src/mixer/MixerList.hxx
@@ -29,6 +29,7 @@ struct MixerPlugin;
 
 extern const MixerPlugin null_mixer_plugin;
 extern const MixerPlugin software_mixer_plugin;
+extern const MixerPlugin android_mixer_plugin;
 extern const MixerPlugin alsa_mixer_plugin;
 extern const MixerPlugin haiku_mixer_plugin;
 extern const MixerPlugin oss_mixer_plugin;
diff --git a/src/mixer/plugins/AndroidMixerPlugin.cxx b/src/mixer/plugins/AndroidMixerPlugin.cxx
new file mode 100644
index 000000000..b24b64fab
--- /dev/null
+++ b/src/mixer/plugins/AndroidMixerPlugin.cxx
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2003-2020 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "mixer/MixerInternal.hxx"
+#include "filter/plugins/VolumeFilterPlugin.hxx"
+#include "pcm/Volume.hxx"
+#include "android/Context.hxx"
+#include "android/AudioManager.hxx"
+
+#include "Main.hxx"
+
+#include <cassert>
+#include <cmath>
+
+class AndroidMixer final : public Mixer {
+	AudioManager *audioManager;
+	int currentVolume;
+	int maxAndroidVolume;
+	int lastAndroidVolume;
+public:
+	explicit AndroidMixer(MixerListener &_listener);
+
+	~AndroidMixer() override;
+
+	/* virtual methods from class Mixer */
+	void Open() override {
+	}
+
+	void Close() noexcept override {
+	}
+
+	int GetVolume() override;
+
+	void SetVolume(unsigned volume) override;
+};
+
+static Mixer *
+android_mixer_init([[maybe_unused]] EventLoop &event_loop,
+		    [[maybe_unused]] AudioOutput &ao,
+		    MixerListener &listener,
+		    [[maybe_unused]] const ConfigBlock &block)
+{
+	return new AndroidMixer(listener);
+}
+
+AndroidMixer::AndroidMixer(MixerListener &_listener)
+	:Mixer(android_mixer_plugin, _listener)
+{
+	JNIEnv *env = Java::GetEnv();
+	audioManager = context->GetAudioManager(env);
+
+	maxAndroidVolume = audioManager->GetMaxVolume();
+	if (maxAndroidVolume != 0)
+	{
+		lastAndroidVolume = audioManager->GetVolume(env);
+		currentVolume = 100 * lastAndroidVolume / maxAndroidVolume;
+	}
+}
+
+AndroidMixer::~AndroidMixer()
+{
+	delete audioManager;
+}
+
+int
+AndroidMixer::GetVolume()
+{
+	JNIEnv *env = Java::GetEnv();
+	if (maxAndroidVolume == 0)
+		return -1;
+
+	// The android volume index (or scale) is very likely inferior to the
+	// MPD one (100). The last volume set by MPD is saved into
+	// currentVolume, this volume is returned instead of the Android one
+	// when the Android mixer was not touched by an other application. This
+	// allows to fake a 0..100 scale from MPD.
+
+	int volume = audioManager->GetVolume(env);
+	if (volume == lastAndroidVolume)
+		return currentVolume;
+
+	return 100 * volume / maxAndroidVolume;
+}
+
+void
+AndroidMixer::SetVolume(unsigned newVolume)
+{
+	JNIEnv *env = Java::GetEnv();
+	if (maxAndroidVolume == 0)
+		return;
+	currentVolume = newVolume;
+	lastAndroidVolume = currentVolume * maxAndroidVolume / 100;
+	audioManager->SetVolume(env, lastAndroidVolume);
+
+}
+
+const MixerPlugin android_mixer_plugin = {
+	android_mixer_init,
+	true,
+};
diff --git a/src/mixer/plugins/meson.build b/src/mixer/plugins/meson.build
index eda7c8e2a..ed9df39b3 100644
--- a/src/mixer/plugins/meson.build
+++ b/src/mixer/plugins/meson.build
@@ -34,6 +34,10 @@ if is_windows
   mixer_plugins_sources += 'WinmmMixerPlugin.cxx'
 endif
 
+if is_android
+  mixer_plugins_sources += 'AndroidMixerPlugin.cxx'
+endif
+
 mixer_plugins = static_library(
   'mixer_plugins',
   mixer_plugins_sources,
diff --git a/src/output/plugins/sles/SlesOutputPlugin.cxx b/src/output/plugins/sles/SlesOutputPlugin.cxx
index 89ad1b7cd..3b345e2e0 100644
--- a/src/output/plugins/sles/SlesOutputPlugin.cxx
+++ b/src/output/plugins/sles/SlesOutputPlugin.cxx
@@ -27,6 +27,7 @@
 #include "thread/Cond.hxx"
 #include "util/Domain.hxx"
 #include "util/ByteOrder.hxx"
+#include "mixer/MixerList.hxx"
 #include "Log.hxx"
 
 #include <SLES/OpenSLES.h>
@@ -412,5 +413,5 @@ const struct AudioOutputPlugin sles_output_plugin = {
 	"sles",
 	sles_test_default_device,
 	SlesOutput::Create,
-	nullptr,
+	&android_mixer_plugin,
 };
diff --git a/src/playlist/plugins/RssPlaylistPlugin.cxx b/src/playlist/plugins/RssPlaylistPlugin.cxx
index 8295b05b0..15f29cf7c 100644
--- a/src/playlist/plugins/RssPlaylistPlugin.cxx
+++ b/src/playlist/plugins/RssPlaylistPlugin.cxx
@@ -160,6 +160,7 @@ static const char *const rss_suffixes[] = {
 
 static const char *const rss_mime_types[] = {
 	"application/rss+xml",
+	"application/xml",
 	"text/xml",
 	nullptr
 };
diff --git a/src/tag/Pool.cxx b/src/tag/Pool.cxx
index f0f5c5e8d..5e2af0c79 100644
--- a/src/tag/Pool.cxx
+++ b/src/tag/Pool.cxx
@@ -32,7 +32,7 @@
 
 Mutex tag_pool_lock;
 
-static constexpr size_t NUM_SLOTS = 4093;
+static constexpr size_t NUM_SLOTS = 16127;
 
 struct TagPoolSlot {
 	TagPoolSlot *next;
diff --git a/src/time/Convert.cxx b/src/time/Convert.cxx
index 2351f4b52..352c60604 100644
--- a/src/time/Convert.cxx
+++ b/src/time/Convert.cxx
@@ -77,15 +77,15 @@ static time_t
 GetTimeZoneOffset() noexcept
 {
 	time_t t = 1234567890;
-	struct tm tm;
-	tm.tm_isdst = 0;
 #ifdef _WIN32
 	struct tm *p = gmtime(&t);
 #else
+	struct tm tm;
+	tm.tm_isdst = 0;
 	struct tm *p = &tm;
 	gmtime_r(&t, p);
 #endif
-	return t - mktime(&tm);
+	return t - mktime(p);
 }
 
 #endif /* !__GLIBC__ */
diff --git a/test/ContainerScan.cxx b/test/ContainerScan.cxx
index 5a7cc41df..886adb0ba 100644
--- a/test/ContainerScan.cxx
+++ b/test/ContainerScan.cxx
@@ -23,6 +23,7 @@
 #include "decoder/DecoderList.hxx"
 #include "decoder/DecoderPlugin.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "fs/io/StdioOutputStream.hxx"
 #include "fs/io/BufferedOutputStream.hxx"
 #include "util/PrintException.hxx"
@@ -63,7 +64,7 @@ try {
 		return EXIT_FAILURE;
 	}
 
-	const Path path = Path::FromFS(argv[1]);
+	const FromNarrowPath path = argv[1];
 
 	const ScopeDecoderPluginsInit decoder_plugins_init({});
 
diff --git a/test/DumpDatabase.cxx b/test/DumpDatabase.cxx
index 402aa31fe..05b98bd1d 100644
--- a/test/DumpDatabase.cxx
+++ b/test/DumpDatabase.cxx
@@ -29,6 +29,7 @@
 #include "ConfigGlue.hxx"
 #include "tag/Config.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "event/Thread.hxx"
 #include "util/ScopeExit.hxx"
 #include "util/PrintException.hxx"
@@ -106,7 +107,7 @@ try {
 		return 1;
 	}
 
-	const Path config_path = Path::FromFS(argv[1]);
+	const FromNarrowPath config_path = argv[1];
 	const char *const plugin_name = argv[2];
 
 	const DatabasePlugin *plugin = GetDatabasePluginByName(plugin_name);
diff --git a/test/ReadApeTags.cxx b/test/ReadApeTags.cxx
index 90f50a46b..1ac4957d5 100644
--- a/test/ReadApeTags.cxx
+++ b/test/ReadApeTags.cxx
@@ -21,6 +21,7 @@
 #include "tag/ApeLoader.hxx"
 #include "thread/Mutex.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "input/InputStream.hxx"
 #include "input/LocalOpen.hxx"
 #include "util/StringView.hxx"
@@ -58,7 +59,7 @@ try {
 		return EXIT_FAILURE;
 	}
 
-	const Path path = Path::FromFS(argv[1]);
+	const FromNarrowPath path = argv[1];
 
 	Mutex mutex;
 
diff --git a/test/ShutdownHandler.hxx b/test/ShutdownHandler.hxx
index 4215275fd..41f44d3c7 100644
--- a/test/ShutdownHandler.hxx
+++ b/test/ShutdownHandler.hxx
@@ -29,8 +29,8 @@ public:
 };
 
 #ifdef _WIN32
-ShutdownHandler::ShutdownHandler(EventLoop &loop) {}
-ShutdownHandler::~ShutdownHandler() {}
+inline ShutdownHandler::ShutdownHandler(EventLoop &) {}
+inline ShutdownHandler::~ShutdownHandler() {}
 #endif
 
 #endif
diff --git a/test/WriteFile.cxx b/test/WriteFile.cxx
index 587eecdb2..4832ee17e 100644
--- a/test/WriteFile.cxx
+++ b/test/WriteFile.cxx
@@ -18,6 +18,7 @@
  */
 
 #include "fs/io/FileOutputStream.hxx"
+#include "fs/NarrowPath.hxx"
 #include "util/PrintException.hxx"
 
 #include <cerrno>
@@ -55,7 +56,7 @@ try {
 		return EXIT_FAILURE;
 	}
 
-	const Path path = Path::FromFS(argv[1]);
+	const FromNarrowPath path = argv[1];
 
 	FileOutputStream fos(path);
 
diff --git a/test/dump_playlist.cxx b/test/dump_playlist.cxx
index 34892a557..6f8dadabe 100644
--- a/test/dump_playlist.cxx
+++ b/test/dump_playlist.cxx
@@ -28,6 +28,7 @@
 #include "playlist/PlaylistRegistry.hxx"
 #include "playlist/PlaylistPlugin.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "fs/io/BufferedOutputStream.hxx"
 #include "fs/io/StdioOutputStream.hxx"
 #include "thread/Cond.hxx"
@@ -54,7 +55,7 @@ try {
 		return EXIT_FAILURE;
 	}
 
-	const Path config_path = Path::FromFS(argv[1]);
+	const FromNarrowPath config_path = argv[1];
 	uri = argv[2];
 
 	/* initialize MPD */
diff --git a/test/meson.build b/test/meson.build
index 0f01aca3e..e3f2a6fa6 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -24,6 +24,7 @@ gtest_dep = declare_dependency(
 )
 
 subdir('net')
+subdir('time')
 
 executable(
   'read_conf',
@@ -52,19 +53,6 @@ test('TestUtil', executable(
   ],
 ))
 
-test(
-  'TestTime',
-  executable(
-    'TestTime',
-    'TestISO8601.cxx',
-    include_directories: inc,
-    dependencies: [
-      time_dep,
-      gtest_dep,
-    ],
-  ),
-)
-
 test('TestRewindInputStream', executable(
   'TestRewindInputStream',
   'TestRewindInputStream.cxx',
@@ -326,6 +314,11 @@ if curl_dep.found()
     include_directories: inc,
     dependencies: [
       curl_dep,
+
+      # Explicitly linking with zlib here works around a linker
+      # failure on Windows, because our Windows CURL build is
+      # statically linked and thus declares no dependency on zlib
+      zlib_dep,
     ],
   )
 
diff --git a/test/net/TestIPv4Address.cxx b/test/net/TestIPv4Address.cxx
index aa7f727be..a6c05e681 100644
--- a/test/net/TestIPv4Address.cxx
+++ b/test/net/TestIPv4Address.cxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2019 Max Kellermann <max.kellermann@gmail.com>
+ * Copyright 2012-2020 Max Kellermann <max.kellermann@gmail.com>
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -34,13 +34,22 @@
 
 #include <stdexcept>
 
+#ifndef _WIN32
 #include <arpa/inet.h>
+#endif
 
 static std::string
 ToString(const struct in_addr &a)
 {
+#ifdef _WIN32
+	/* on mingw32, the parameter is non-const (PVOID) */
+	const auto p = const_cast<struct in_addr *>(&a);
+#else
+	const auto p = &a;
+#endif
+
 	char buffer[256];
-	const char *result = inet_ntop(AF_INET, &a, buffer, sizeof(buffer));
+	const char *result = inet_ntop(AF_INET, p, buffer, sizeof(buffer));
 	if (result == nullptr)
 		throw std::runtime_error("inet_ntop() failed");
 	return result;
diff --git a/test/net/TestIPv6Address.cxx b/test/net/TestIPv6Address.cxx
index 325d25bbf..cdadf6a7f 100644
--- a/test/net/TestIPv6Address.cxx
+++ b/test/net/TestIPv6Address.cxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2019 Max Kellermann <max.kellermann@gmail.com>
+ * Copyright 2012-2020 Max Kellermann <max.kellermann@gmail.com>
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -34,13 +34,22 @@
 
 #include <stdexcept>
 
+#ifndef _WIN32
 #include <arpa/inet.h>
+#endif
 
 static std::string
 ToString(const struct in6_addr &a)
 {
+#ifdef _WIN32
+	/* on mingw32, the parameter is non-const (PVOID) */
+	const auto p = const_cast<struct in6_addr *>(&a);
+#else
+	const auto p = &a;
+#endif
+
 	char buffer[256];
-	const char *result = inet_ntop(AF_INET6, &a, buffer, sizeof(buffer));
+	const char *result = inet_ntop(AF_INET6, p, buffer, sizeof(buffer));
 	if (result == nullptr)
 		throw std::runtime_error("inet_ntop() failed");
 	return result;
diff --git a/test/read_conf.cxx b/test/read_conf.cxx
index 5350102f0..eaec60a6d 100644
--- a/test/read_conf.cxx
+++ b/test/read_conf.cxx
@@ -21,6 +21,7 @@
 #include "config/Param.hxx"
 #include "config/File.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "util/PrintException.hxx"
 #include "util/RuntimeError.hxx"
 
@@ -34,7 +35,7 @@ try {
 		return EXIT_FAILURE;
 	}
 
-	const Path config_path = Path::FromFS(argv[1]);
+	const FromNarrowPath config_path = argv[1];
 	const char *name = argv[2];
 
 	const auto option = ParseConfigOptionName(name);
diff --git a/test/read_tags.cxx b/test/read_tags.cxx
index c47c37665..7a1c32e9b 100644
--- a/test/read_tags.cxx
+++ b/test/read_tags.cxx
@@ -27,6 +27,7 @@
 #include "tag/Handler.hxx"
 #include "tag/Generic.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "pcm/AudioFormat.hxx"
 #include "util/ScopeExit.hxx"
 #include "util/StringBuffer.hxx"
@@ -97,7 +98,7 @@ try {
 	}
 
 	decoder_name = argv[1];
-	const Path path = Path::FromFS(argv[2]);
+	const char *path = argv[2];
 
 	EventThread io_thread;
 	io_thread.Start();
@@ -116,7 +117,7 @@ try {
 	DumpTagHandler h;
 	bool success;
 	try {
-		success = plugin->ScanFile(path, h);
+		success = plugin->ScanFile(FromNarrowPath(path), h);
 	} catch (...) {
 		PrintException(std::current_exception());
 		success = false;
@@ -126,7 +127,7 @@ try {
 	InputStreamPtr is;
 
 	if (!success && plugin->scan_stream != nullptr) {
-		is = InputStream::OpenReady(path.c_str(), mutex);
+		is = InputStream::OpenReady(path, mutex);
 		success = plugin->ScanStream(*is, h);
 	}
 
@@ -139,7 +140,7 @@ try {
 		if (is)
 			ScanGenericTags(*is, h);
 		else
-			ScanGenericTags(path, h);
+			ScanGenericTags(FromNarrowPath(path), h);
 	}
 
 	return 0;
diff --git a/test/run_decoder.cxx b/test/run_decoder.cxx
index 8b8e6ef61..1147e4faf 100644
--- a/test/run_decoder.cxx
+++ b/test/run_decoder.cxx
@@ -26,6 +26,7 @@
 #include "input/Init.hxx"
 #include "input/InputStream.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "pcm/AudioFormat.hxx"
 #include "util/OptionDef.hxx"
 #include "util/OptionParser.hxx"
@@ -44,7 +45,7 @@ struct CommandLine {
 	const char *decoder = nullptr;
 	const char *uri = nullptr;
 
-	Path config_path = nullptr;
+	FromNarrowPath config_path;
 
 	bool verbose = false;
 
@@ -72,7 +73,7 @@ ParseCommandLine(int argc, char **argv)
 	while (auto o = option_parser.Next()) {
 		switch (Option(o.index)) {
 		case OPTION_CONFIG:
-			c.config_path = Path::FromFS(o.value);
+			c.config_path = o.value;
 			break;
 
 		case OPTION_VERBOSE:
@@ -205,7 +206,7 @@ try {
 	MyDecoderClient client(c.seek_where);
 	if (plugin->file_decode != nullptr) {
 		try {
-			plugin->FileDecode(client, Path::FromFS(c.uri));
+			plugin->FileDecode(client, FromNarrowPath(c.uri));
 		} catch (StopDecoder) {
 		}
 	} else if (plugin->stream_decode != nullptr) {
diff --git a/test/run_filter.cxx b/test/run_filter.cxx
index 77c76106a..44b54b88c 100644
--- a/test/run_filter.cxx
+++ b/test/run_filter.cxx
@@ -19,6 +19,7 @@
 
 #include "ConfigGlue.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "filter/LoadOne.hxx"
 #include "filter/Filter.hxx"
 #include "filter/Prepared.hxx"
@@ -123,7 +124,7 @@ try {
 		return EXIT_FAILURE;
 	}
 
-	const Path config_path = Path::FromFS(argv[1]);
+	const FromNarrowPath config_path = argv[1];
 
 	AudioFormat audio_format(44100, SampleFormat::S16, 2);
 
diff --git a/test/run_gunzip.cxx b/test/run_gunzip.cxx
index fe719bfa4..a5227df4b 100644
--- a/test/run_gunzip.cxx
+++ b/test/run_gunzip.cxx
@@ -20,6 +20,7 @@
 #include "fs/io/GunzipReader.hxx"
 #include "fs/io/FileReader.hxx"
 #include "fs/io/StdioOutputStream.hxx"
+#include "fs/NarrowPath.hxx"
 #include "util/PrintException.hxx"
 
 #include <stdio.h>
@@ -62,7 +63,7 @@ try {
 		return EXIT_FAILURE;
 	}
 
-	Path path = Path::FromFS(argv[1]);
+	FromNarrowPath path = argv[1];
 
 	CopyGunzip(stdout, path);
 	return EXIT_SUCCESS;
diff --git a/test/run_input.cxx b/test/run_input.cxx
index d10a17518..20fe8c2d7 100644
--- a/test/run_input.cxx
+++ b/test/run_input.cxx
@@ -32,6 +32,7 @@
 #include "Log.hxx"
 #include "LogBackend.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "fs/io/BufferedOutputStream.hxx"
 #include "fs/io/StdioOutputStream.hxx"
 #include "util/ConstBuffer.hxx"
@@ -51,7 +52,7 @@
 struct CommandLine {
 	const char *uri = nullptr;
 
-	Path config_path = nullptr;
+	FromNarrowPath config_path;
 
 	bool verbose = false;
 
@@ -79,7 +80,7 @@ ParseCommandLine(int argc, char **argv)
 	while (auto o = option_parser.Next()) {
 		switch (Option(o.index)) {
 		case OPTION_CONFIG:
-			c.config_path = Path::FromFS(o.value);
+			c.config_path = o.value;
 			break;
 
 		case OPTION_VERBOSE:
diff --git a/test/run_output.cxx b/test/run_output.cxx
index 232c94d38..7c0730152 100644
--- a/test/run_output.cxx
+++ b/test/run_output.cxx
@@ -23,6 +23,7 @@
 #include "ConfigGlue.hxx"
 #include "event/Thread.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "pcm/AudioParser.hxx"
 #include "pcm/AudioFormat.hxx"
 #include "util/StringBuffer.hxx"
@@ -111,7 +112,7 @@ try {
 		return EXIT_FAILURE;
 	}
 
-	const Path config_path = Path::FromFS(argv[1]);
+	const FromNarrowPath config_path = argv[1];
 
 	AudioFormat audio_format(44100, SampleFormat::S16, 2);
 
diff --git a/test/time/TestConvert.cxx b/test/time/TestConvert.cxx
new file mode 100644
index 000000000..ff31d7097
--- /dev/null
+++ b/test/time/TestConvert.cxx
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 Max Kellermann <max.kellermann@gmail.com>
+ * All rights reserved.
+ *
+ * author: Max Kellermann <mk@cm4all.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+ * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "time/Convert.hxx"
+
+#include <gtest/gtest.h>
+
+static constexpr time_t times[] = {
+	1234567890,
+	1580566807,
+	1585750807,
+	1590934807,
+};
+
+TEST(Time, LocalTime)
+{
+	/* convert back and forth using local time zone */
+
+	for (const auto t : times) {
+		auto tp = std::chrono::system_clock::from_time_t(t);
+		auto tm = LocalTime(tp);
+		EXPECT_EQ(MakeTime(tm), tp);
+	}
+}
+
+TEST(Time, GmTime)
+{
+	/* convert back and forth using UTC */
+
+	for (const auto t : times) {
+		auto tp = std::chrono::system_clock::from_time_t(t);
+		auto tm = GmTime(tp);
+		EXPECT_EQ(std::chrono::system_clock::to_time_t(TimeGm(tm)),
+			  t);
+	}
+}
diff --git a/test/TestISO8601.cxx b/test/time/TestISO8601.cxx
similarity index 100%
rename from test/TestISO8601.cxx
rename to test/time/TestISO8601.cxx
diff --git a/test/time/meson.build b/test/time/meson.build
new file mode 100644
index 000000000..53b583cf1
--- /dev/null
+++ b/test/time/meson.build
@@ -0,0 +1,17 @@
+test_time_sources = [
+  'TestConvert.cxx',
+  'TestISO8601.cxx',
+]
+
+test(
+  'TestTime',
+  executable(
+    'TestTime',
+    test_time_sources,
+    include_directories: inc,
+    dependencies: [
+      time_dep,
+      gtest_dep,
+    ],
+  ),
+)