aboutsummaryrefslogtreecommitdiff
path: root/Src/Plugins/Library/ml_pmp/PmpDevice.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Src/Plugins/Library/ml_pmp/PmpDevice.cpp')
-rw-r--r--Src/Plugins/Library/ml_pmp/PmpDevice.cpp557
1 files changed, 557 insertions, 0 deletions
diff --git a/Src/Plugins/Library/ml_pmp/PmpDevice.cpp b/Src/Plugins/Library/ml_pmp/PmpDevice.cpp
new file mode 100644
index 00000000..3ef7eae6
--- /dev/null
+++ b/Src/Plugins/Library/ml_pmp/PmpDevice.cpp
@@ -0,0 +1,557 @@
+#include "main.h"
+#include "DeviceView.h"
+#include "api__ml_pmp.h"
+#include "../nu/refcount.h"
+#include "..\..\General\gen_ml/ml_ipc_0313.h"
+#include "DeviceCommands.h"
+#include "resource1.h"
+#include <strsafe.h>
+
+// known commands
+static const char *DEVICE_CMD_VIEW_OPEN = "view_open";
+static const char *DEVICE_CMD_SYNC = "sync";
+static const char *DEVICE_CMD_AUTOFILL = "autofill";
+static const char *DEVICE_CMD_PLAYLIST_CREATE = "playlist_create";
+static const char *DEVICE_CMD_RENAME = "rename";
+static const char *DEVICE_CMD_PREFERENCES = "preferences";
+static const char *DEVICE_CMD_EJECT = "eject";
+static const char *DEVICE_CMD_REMOVE = "remove";
+static const char *DEVICE_CMD_TRANSFER = "transfer";
+static const char *DEVICE_CMD_HIDE = "hide";
+
+extern void UpdateDevicesListView(bool softUpdate);
+
+// we're going to share the command enum stuff for all devices
+
+class PortableDeviceType : public ifc_devicetype
+{
+public:
+ PortableDeviceType()
+ {
+ }
+ const char *GetName()
+ {
+ return "portable";
+ }
+
+ HRESULT GetIcon(wchar_t *buffer, size_t bufferSize, int width, int height)
+ {
+ return E_NOTIMPL;
+ }
+
+ HRESULT GetDisplayName(wchar_t *buffer, size_t bufferSize)
+ {
+ if (NULL == buffer)
+ return E_POINTER;
+
+ WASABI_API_LNGSTRINGW_BUF(IDS_PORTABLE_DEVICE_TYPE, buffer, bufferSize);
+ return S_OK;
+ }
+protected:
+
+#define CBCLASS PortableDeviceType
+ START_DISPATCH_INLINE;
+ CB(API_GETNAME, GetName);
+ CB(API_GETICON, GetIcon);
+ CB(API_GETDISPLAYNAME, GetDisplayName);
+ END_DISPATCH;
+#undef CBCLASS
+};
+
+class USBDeviceConnection : public ifc_deviceconnection
+{
+public:
+ USBDeviceConnection()
+ {
+ }
+ const char *GetName()
+ {
+ return "usb";
+ }
+
+ HRESULT GetIcon(wchar_t *buffer, size_t bufferSize, int width, int height)
+ {
+ if (FALSE == FormatResProtocol(MAKEINTRESOURCE(IDB_USB),
+ L"PNG", buffer, bufferSize))
+ {
+ return E_FAIL;
+ }
+
+ return S_OK;
+ }
+
+ HRESULT GetDisplayName(wchar_t *buffer, size_t bufferSize)
+ {
+ if (NULL == buffer)
+ return E_POINTER;
+
+ WASABI_API_LNGSTRINGW_BUF(IDS_DEVICE_CONNECTION_USB, buffer, bufferSize);
+ return S_OK;
+ }
+protected:
+
+#define CBCLASS USBDeviceConnection
+ START_DISPATCH_INLINE;
+ CB(API_GETNAME, GetName);
+ CB(API_GETICON, GetIcon);
+ CB(API_GETDISPLAYNAME, GetDisplayName);
+ END_DISPATCH;
+#undef CBCLASS
+};
+
+static PortableDeviceType portable_device_type;
+static USBDeviceConnection usb_connection;
+static PortableCommand registered_commands[] =
+{
+ PortableCommand(DEVICE_CMD_VIEW_OPEN, IDS_DEVICE_CMD_VIEW_OPEN, IDS_DEVICE_CMD_VIEW_OPEN_DESC),
+ PortableCommand(DEVICE_CMD_SYNC, IDS_DEVICE_CMD_SYNC, IDS_DEVICE_CMD_SYNC_DESC),
+ PortableCommand(DEVICE_CMD_TRANSFER, IDS_DEVICE_CMD_TRANSFER, IDS_DEVICE_CMD_TRANSFER_DESC),
+ PortableCommand(DEVICE_CMD_EJECT, IDS_DEVICE_CMD_EJECT, IDS_DEVICE_CMD_EJECT_DESC),
+ PortableCommand(DEVICE_CMD_REMOVE, IDS_DEVICE_CMD_REMOVE, IDS_DEVICE_CMD_REMOVE_DESC),
+ PortableCommand(DEVICE_CMD_RENAME, IDS_DEVICE_CMD_RENAME, IDS_DEVICE_CMD_RENAME_DESC),
+ PortableCommand(DEVICE_CMD_AUTOFILL, IDS_DEVICE_CMD_AUTOFILL, IDS_DEVICE_CMD_AUTOFILL_DESC),
+ PortableCommand(DEVICE_CMD_PLAYLIST_CREATE, IDS_DEVICE_CMD_PLAYLIST_CREATE, IDS_DEVICE_CMD_PLAYLIST_CREATE_DESC),
+ PortableCommand(DEVICE_CMD_PREFERENCES, IDS_DEVICE_CMD_PREFERENCES, IDS_DEVICE_CMD_PREFERENCES_DESC),
+ PortableCommand(DEVICE_CMD_HIDE, IDS_DEVICE_CMD_HIDE, IDS_DEVICE_CMD_HIDE),
+};
+
+static ifc_devicecommand * _cdecl
+Devices_RegisterCommand(const char *name, void *user)
+{
+ for(size_t i = 0; i < sizeof(registered_commands)/sizeof(*registered_commands); i++)
+ {
+ if (name == registered_commands[i].GetName())
+ {
+ return &registered_commands[i];
+ }
+ }
+ return NULL;
+}
+
+void Devices_Init()
+{
+ if (AGAVE_API_DEVICEMANAGER)
+ {
+ /* register 'portable' device type */
+ ifc_devicetype *type = &portable_device_type;
+ AGAVE_API_DEVICEMANAGER->TypeRegister(&type, 1);
+
+ /* register 'usb' connection type */
+ ifc_deviceconnection *connection = &usb_connection;
+ AGAVE_API_DEVICEMANAGER->ConnectionRegister(&connection, 1);
+
+
+ /* register commands */
+ const char *commands[sizeof(registered_commands)/sizeof(*registered_commands)];
+ for(size_t i = 0; i < sizeof(registered_commands)/sizeof(*registered_commands); i++)
+ {
+ commands[i] = registered_commands[i].GetName();
+ }
+ AGAVE_API_DEVICEMANAGER->CommandRegisterIndirect(commands, sizeof(registered_commands)/sizeof(*registered_commands), Devices_RegisterCommand, NULL);
+ }
+}
+
+int DeviceView::QueryInterface(GUID interface_guid, void **object)
+{
+ if (interface_guid == IFC_Device)
+ {
+ AddRef();
+ *object = (ifc_device *)this;
+ return 0;
+ }
+ return 1;
+}
+
+const char *DeviceView::GetName()
+{
+ return name;
+}
+
+HRESULT DeviceView::GetIcon(wchar_t *buffer, size_t bufferSize, int width, int height)
+{
+ buffer[0]=0;
+ dev->extraActions(DEVICE_GET_ICON, width, height, (intptr_t)buffer);
+ if (buffer[0] == 0)
+ return E_NOTIMPL;
+ else
+ return S_OK;
+
+ return E_NOTIMPL;
+}
+
+HRESULT DeviceView::GetDisplayName(wchar_t *buffer, size_t bufferSize)
+{
+ // TODO sometimes this is erroring on loading
+ dev->getPlaylistName(0, buffer, bufferSize);
+ return S_OK;
+}
+
+const char *DeviceView::GetType()
+{
+ return "portable";
+}
+
+const char *DeviceView::GetDisplayType()
+{
+ return display_type;
+}
+
+const char *DeviceView::GetConnection()
+{
+ return connection_type;
+}
+
+BOOL DeviceView::GetHidden()
+{
+ return FALSE;
+}
+
+HRESULT DeviceView::GetTotalSpace(uint64_t *size)
+{
+ UpdateSpaceInfo(FALSE, TRUE);
+ *size = dev->getDeviceCapacityTotal();
+ return S_OK;
+}
+
+HRESULT DeviceView::GetUsedSpace(uint64_t *size)
+{
+ if (NULL == size)
+ return E_POINTER;
+
+ UpdateSpaceInfo(TRUE, TRUE);
+ *size = usedSpace;
+
+ return S_OK;
+}
+
+BOOL DeviceView::GetAttached()
+{
+ return TRUE; // ml_pmp devices are by default attached
+}
+
+HRESULT DeviceView::Attach(HWND hostWindow)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT DeviceView::Detach(HWND hostWindow)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT DeviceView::EnumerateCommands(ifc_devicesupportedcommandenum **enumerator, DeviceCommandContext context)
+{
+ DeviceCommandInfo commands[32];
+ size_t count;
+
+ if (NULL == enumerator)
+ return E_POINTER;
+
+ count = 0;
+
+ LinkedQueue * txQueue = getTransferQueue(this);
+ if (txQueue == NULL)
+ return E_POINTER;
+
+ // return E_NOTIMPL;
+ if (context == DeviceCommandContext_View)
+ {
+ if (0 == txQueue->GetSize())
+ {
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_SYNC, DeviceCommandFlag_Primary);
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_AUTOFILL, DeviceCommandFlag_None);
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_PLAYLIST_CREATE, DeviceCommandFlag_None);
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_PREFERENCES, DeviceCommandFlag_None);
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_EJECT, DeviceCommandFlag_None);
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_REMOVE, DeviceCommandFlag_None);
+ }
+ }
+ else
+ {
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_VIEW_OPEN, DeviceCommandFlag_Primary);
+
+ DeviceCommandFlags flags = DeviceCommandFlag_None;
+ if (0 != txQueue->GetSize())
+ flags |= DeviceCommandFlag_Disabled;
+
+ if (0 != dev->extraActions(DEVICE_SYNC_UNSUPPORTED,0,0,0))
+ flags |= DeviceCommandFlag_Disabled;
+ SetDeviceCommandInfo(&commands[count++], (!isCloudDevice ? DEVICE_CMD_SYNC : DEVICE_CMD_TRANSFER), flags | DeviceCommandFlag_Group);
+ if (!isCloudDevice) SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_AUTOFILL, flags);
+
+ flags = DeviceCommandFlag_None;
+ if (0 == dev->extraActions(DEVICE_PLAYLISTS_UNSUPPORTED,0,0,0))
+ {
+ // TODO remove once we've got cloud playlists implemented
+ if (!isCloudDevice)
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_PLAYLIST_CREATE, flags | DeviceCommandFlag_Group);
+ }
+
+ // adds a specific menu item to hide the 'local library' source
+ if (isCloudDevice)
+ {
+ char name[128] = {0};
+ if (dev->extraActions(DEVICE_GET_UNIQUE_ID, (intptr_t)name, sizeof(name), 0))
+ {
+ if (!strcmp(name, "local_desktop"))
+ {
+ flags = DeviceCommandFlag_None | DeviceCommandFlag_Group;
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_HIDE, flags);
+ }
+ }
+ }
+
+ bool has_rename = true;
+ flags = DeviceCommandFlag_None;
+ if (0 == dev->extraActions(DEVICE_CAN_RENAME_DEVICE,0,0,0))
+ has_rename = false;
+ else
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_RENAME, flags | DeviceCommandFlag_Group);
+
+ flags = (!has_rename ? DeviceCommandFlag_Group : DeviceCommandFlag_None);
+ SetDeviceCommandInfo(&commands[count++], DEVICE_CMD_PREFERENCES, flags);
+ if (!dev->extraActions(DEVICE_DOES_NOT_SUPPORT_REMOVE,0,0,0))
+ SetDeviceCommandInfo(&commands[count++], (!isCloudDevice ? DEVICE_CMD_EJECT : DEVICE_CMD_REMOVE), flags | DeviceCommandFlag_Group);
+ }
+
+ *enumerator = new DeviceCommandEnumerator(commands, count);
+ if (NULL == *enumerator)
+ return E_OUTOFMEMORY;
+
+ return S_OK;
+}
+
+HRESULT DeviceView::SendCommand(const char *command, HWND hostWindow, ULONG_PTR param)
+{
+ if (!strcmp(command, DEVICE_CMD_EJECT) || !strcmp(command, DEVICE_CMD_REMOVE))
+ {
+ Eject();
+ return S_OK;
+ }
+ else if (!strcmp(command, DEVICE_CMD_SYNC) || !strcmp(command, DEVICE_CMD_TRANSFER))
+ {
+ if (!this->isCloudDevice) Sync();
+ else CloudSync();
+ return S_OK;
+ }
+ else if (!strcmp(command, DEVICE_CMD_AUTOFILL))
+ {
+ Autofill();
+ return S_OK;
+ }
+ else if (!strcmp(command, DEVICE_CMD_RENAME))
+ {
+ if (NULL != treeItem)
+ MLNavItem_EditTitle(plugin.hwndLibraryParent, treeItem);
+ else
+ RenamePlaylist(0);
+ return S_OK;
+ }
+ else if (!strcmp(command, DEVICE_CMD_PLAYLIST_CREATE))
+ {
+ CreatePlaylist();
+ return S_OK;
+ }
+ else if (!strcmp(command, DEVICE_CMD_PREFERENCES))
+ {
+ SENDWAIPC(plugin.hwndWinampParent, IPC_OPENPREFSTOPAGE,(WPARAM)&devPrefsPage);
+ return S_OK;
+ }
+ else if (!strcmp(command, DEVICE_CMD_HIDE))
+ {
+ static int IPC_CLOUD_HIDE_LOCAL = -1;
+ if (IPC_CLOUD_HIDE_LOCAL == -1)
+ IPC_CLOUD_HIDE_LOCAL = SendMessage(plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)&"WinampCloudLocal", IPC_REGISTER_WINAMP_IPCMESSAGE);
+
+ SENDWAIPC(plugin.hwndWinampParent, IPC_CLOUD_HIDE_LOCAL, 0);
+ return S_OK;
+ }
+
+ return E_NOTIMPL;
+}
+
+HRESULT DeviceView::GetCommandFlags(const char *command, DeviceCommandFlags *flags)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT DeviceView::GetActivity(ifc_deviceactivity **activity)
+{
+ LinkedQueue * txQueue = getTransferQueue();
+ if (txQueue == NULL || txQueue->GetSize() == 0)
+ {
+ *activity = 0;
+ return S_FALSE;
+ }
+ AddRef();
+ *activity = this;
+ return S_OK;
+}
+
+HRESULT DeviceView::Advise(ifc_deviceevent *handler)
+{
+ event_handlers.push_back(handler);
+ return S_OK;
+}
+
+HRESULT DeviceView::Unadvise(ifc_deviceevent *handler)
+{
+ //event_handlers.eraseObject(handler);
+ auto it = std::find(event_handlers.begin(), event_handlers.end(), handler);
+ if (it != event_handlers.end())
+ {
+ event_handlers.erase(it);
+ }
+
+ return S_OK;
+}
+
+extern C_ItemList devices;
+extern void UpdateDevicesListView(bool softupdate);
+void DeviceView::SetNavigationItem(void *navigationItem)
+{
+ if (navigationItem)
+ RegisterViews((HNAVITEM)navigationItem);
+}
+
+BOOL DeviceView::GetActive()
+{
+ LinkedQueue * txQueue = getTransferQueue();
+ if (txQueue == NULL || txQueue->GetSize() == 0)
+ return FALSE;
+ return TRUE;
+}
+
+BOOL DeviceView::GetCancelable()
+{
+ return FALSE;
+}
+
+HRESULT DeviceView::GetProgress(unsigned int *percentCompleted)
+{
+ LinkedQueue * txQueue = getTransferQueue();
+ LinkedQueue * finishedTX = getFinishedTransferQueue();
+ int txProgress = getTransferProgress();
+ int size = (txQueue ? txQueue->GetSize() : 0);
+ double num = (100.0 * (double)size) - (double)txProgress;
+ double total = (double)100 * size + 100 * (finishedTX ? finishedTX->GetSize() : 0);
+
+ double percent = (0 != total) ? (((total - num) * 100) / total) : 0;
+
+ *percentCompleted = (unsigned int)percent;
+ return S_OK;
+}
+
+HRESULT DeviceView::Activity_GetDisplayName(wchar_t *buffer, size_t bufferMax)
+{
+ WASABI_API_LNGSTRINGW_BUF(IDS_TRANSFERRING, buffer, bufferMax);
+ return S_OK;
+}
+
+HRESULT DeviceView::Activity_GetStatus(wchar_t *buffer, size_t bufferMax)
+{
+ WASABI_API_LNGSTRINGW_BUF(IDS_TRANSFERRING_DESC, buffer, bufferMax);
+ return S_OK;
+}
+
+HRESULT DeviceView::Cancel(HWND hostWindow)
+{
+// threadKillswitch = 1; // TODO: i think this is how to do it
+ //transferContext.WaitForKill();
+ return S_OK;
+}
+
+HRESULT DeviceView::GetDropSupported(unsigned int dataType)
+{
+ if (dataType == ML_TYPE_ITEMRECORDLISTW
+ || dataType == ML_TYPE_ITEMRECORDLIST
+ || dataType == ML_TYPE_PLAYLIST
+ || dataType == ML_TYPE_PLAYLISTS
+ || dataType == ML_TYPE_FILENAMES
+ || dataType == ML_TYPE_FILENAMESW)
+ return S_OK;
+ return E_FAIL;
+}
+
+HRESULT DeviceView::Drop(void *data, unsigned int dataType)
+{
+ return (HRESULT)TransferFromML(dataType,data,E_FAIL,S_OK);
+}
+
+HRESULT DeviceView::SetDisplayName(const wchar_t *displayName, bool force = 0)
+{
+ if((0 == force && 0 == dev->extraActions(DEVICE_CAN_RENAME_DEVICE,0,0,0)))
+ return E_FAIL;
+
+ dev->setPlaylistName(0, displayName);
+ free(devPrefsPage.name);
+ devPrefsPage.name = _wcsdup(displayName);
+ SENDWAIPC(plugin.hwndWinampParent, IPC_UPDATE_PREFS_DLGW, (WPARAM)&devPrefsPage);
+
+ DevicePropertiesChanges();
+ UpdateDevicesListView(false);
+
+ OnNameChanged(displayName);
+
+ return S_OK;
+}
+
+HRESULT DeviceView::GetModel(wchar_t *buffer, size_t bufferSize)
+{
+ if (NULL == buffer)
+ return E_POINTER;
+
+ buffer[0] = L'\0';
+
+ if(0 == dev->extraActions(DEVICE_GET_MODEL, (intptr_t)buffer, bufferSize, 0))
+ return E_NOTIMPL;
+
+ return S_OK;
+}
+
+HRESULT DeviceView::GetStatus(wchar_t *buffer, size_t bufferSize)
+{
+ return E_NOTIMPL;
+}
+
+#define CBCLASS DeviceView
+START_MULTIPATCH;
+START_PATCH(PATCH_IFC_DEVICE)
+M_CB(PATCH_IFC_DEVICE, ifc_device, QUERYINTERFACE, QueryInterface);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETNAME, GetName);
+M_CB(PATCH_IFC_DEVICE, ifc_device, ifc_deviceobject::API_GETDISPLAYNAME, GetDisplayName);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETICON, GetIcon);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETTYPE, GetType);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETDISPLAYTYPE, GetDisplayType);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETCONNECTION, GetConnection);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETTOTALSPACE, GetTotalSpace);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETUSEDSPACE, GetUsedSpace);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_ENUMERATECOMMANDS, EnumerateCommands);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_SENDCOMMAND, SendCommand);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETATTACHED, GetAttached);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_ATTACH, Attach);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_DETACH, Detach);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_ADVISE, Advise);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_UNADVISE, Unadvise);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_CREATEVIEW, CreateView);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETACTIVITY, GetActivity);
+M_VCB(PATCH_IFC_DEVICE, ifc_device, API_SETNAVIGATIONITEM, SetNavigationItem);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETDROPSUPPORTED, GetDropSupported);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_DROP, Drop);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_SETDISPLAYNAME, SetDisplayName);
+M_CB(PATCH_IFC_DEVICE, ifc_device, API_GETMODEL, GetModel);
+M_CB(PATCH_IFC_DEVICE, ifc_device, ifc_device::API_GETSTATUS, GetStatus);
+M_CB(PATCH_IFC_DEVICE, ifc_device, ADDREF, AddRef);
+M_CB(PATCH_IFC_DEVICE, ifc_device, RELEASE, Release);
+NEXT_PATCH(PATCH_IFC_DEVICEACTIVITY)
+M_CB(PATCH_IFC_DEVICEACTIVITY, ifc_deviceactivity, API_GETACTIVE, GetActive);
+M_CB(PATCH_IFC_DEVICEACTIVITY, ifc_deviceactivity, API_GETCANCELABLE, GetCancelable);
+M_CB(PATCH_IFC_DEVICEACTIVITY, ifc_deviceactivity, API_GETPROGRESS, GetProgress);
+M_CB(PATCH_IFC_DEVICEACTIVITY, ifc_deviceactivity, ifc_deviceactivity::API_GETDISPLAYNAME, Activity_GetDisplayName);
+M_CB(PATCH_IFC_DEVICEACTIVITY, ifc_deviceactivity, ifc_deviceactivity::API_GETSTATUS, Activity_GetStatus);
+M_CB(PATCH_IFC_DEVICEACTIVITY, ifc_deviceactivity, API_CANCEL, Cancel);
+M_CB(PATCH_IFC_DEVICEACTIVITY, ifc_deviceactivity, ADDREF, AddRef);
+M_CB(PATCH_IFC_DEVICEACTIVITY, ifc_deviceactivity, RELEASE, Release);
+END_PATCH
+END_MULTIPATCH;
+#undef CBCLASS \ No newline at end of file