diff options
author | Max Kellermann <max@musicpd.org> | 2020-04-02 18:02:10 +0200 |
---|---|---|
committer | Max Kellermann <max@musicpd.org> | 2020-04-02 18:02:10 +0200 |
commit | 12b97bbe3896df558b3b8f52e8d9d495058aa996 (patch) | |
tree | 4430cf6fcd89a7c9d389f1f903c927b6d5297d1a | |
parent | 7d7bd51bc0a4748593e774decc91a1dbc939fd90 (diff) | |
parent | 5ccfcffcc124e406233359fe8fe65b704b98b8c8 (diff) |
Merge tag 'v0.21.22'
release v0.21.22
48 files changed, 605 insertions, 97 deletions
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" @@ -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 index cd0897c1a..cd0897c1a 100644 --- a/test/TestISO8601.cxx +++ b/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, + ], + ), +) |