root/src/preferences/network/NetworkWindow.cpp
/*
 * Copyright 2004-2019 Haiku Inc., All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 *      Authors:
 *              Adrien Destugues, <pulkomandy@pulkomandy.tk>
 *              Axel Dörfler, <axeld@pinc-software.de>
 *              Alexander von Gluck, <kallisti5@unixzen.com>
 */


#include "NetworkWindow.h"

#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <Alert.h>
#include <Application.h>
#include <Button.h>
#include <Catalog.h>
#include <CheckBox.h>
#include <ControlLook.h>
#include <Deskbar.h>
#include <Directory.h>
#include <LayoutBuilder.h>
#include <NetworkDevice.h>
#include <NetworkInterface.h>
#include <NetworkNotifications.h>
#include <NetworkRoster.h>
#include <OutlineListView.h>
#include <Path.h>
#include <PathFinder.h>
#include <PathMonitor.h>
#include <Roster.h>
#include <ScrollView.h>
#include <StringItem.h>
#include <SymLink.h>

#define ENABLE_PROFILES 0
#if ENABLE_PROFILES
#       include <PopUpMenu.h>
#endif

#include "InterfaceListItem.h"
#include "InterfaceView.h"
#include "ServiceListItem.h"


const char* kNetworkStatusSignature = "application/x-vnd.Haiku-NetworkStatus";

static const uint32 kMsgProfileSelected = 'prof';
static const uint32 kMsgProfileManage = 'mngp';
static const uint32 kMsgProfileNew = 'newp';
static const uint32 kMsgRevert = 'rvrt';
static const uint32 kMsgToggleReplicant = 'trep';
static const uint32 kMsgItemSelected = 'ItSl';

BMessenger gNetworkWindow;


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT   "NetworkWindow"


class TitleItem : public BStringItem {
public:
        TitleItem(const char* title)
                :
                BStringItem(title)
        {
        }

        void DrawItem(BView* owner, BRect bounds, bool complete)
        {
                owner->SetFont(be_bold_font);
                BStringItem::DrawItem(owner, bounds, complete);
                owner->SetFont(be_plain_font);
        }

        void Update(BView* owner, const BFont* font)
        {
                BStringItem::Update(owner, be_bold_font);
        }
};


// #pragma mark -


NetworkWindow::NetworkWindow()
        :
        BWindow(BRect(100, 100, 750, 400), B_TRANSLATE_SYSTEM_NAME("Network"),
                B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_NOT_ZOOMABLE
                        | B_AUTO_UPDATE_SIZE_LIMITS),
        fServicesItem(NULL),
        fDialUpItem(NULL),
        fVPNItem(NULL),
        fOtherItem(NULL)
{
        // Profiles section
#if ENABLE_PROFILES
        BPopUpMenu* profilesPopup = new BPopUpMenu("<none>");
        _BuildProfilesMenu(profilesPopup, kMsgProfileSelected);

        BMenuField* profilesMenuField = new BMenuField("profiles_menu",
                B_TRANSLATE("Profile:"), profilesPopup);

        profilesMenuField->SetFont(be_bold_font);
        profilesMenuField->SetEnabled(false);
#endif

        // Settings section

        fRevertButton = new BButton("revert", B_TRANSLATE("Revert"),
                new BMessage(kMsgRevert));

        BMessage* message = new BMessage(kMsgToggleReplicant);
        BCheckBox* showReplicantCheckBox = new BCheckBox("showReplicantCheckBox",
                B_TRANSLATE("Show network status in Deskbar"), message);
        showReplicantCheckBox->SetExplicitMaxSize(
                BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
        showReplicantCheckBox->SetValue(_IsReplicantInstalled());

        fListView = new BOutlineListView("list", B_SINGLE_SELECTION_LIST,
                B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS | B_NAVIGABLE);
        fListView->SetSelectionMessage(new BMessage(kMsgItemSelected));

        BScrollView* scrollView = new BScrollView("ScrollView", fListView,
                0, false, true);
        scrollView->SetExplicitMaxSize(BSize(B_SIZE_UNSET, B_SIZE_UNLIMITED));

        fAddOnShellView = new BView("add-on shell", 0,
                new BGroupLayout(B_VERTICAL));
        fAddOnShellView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
        fAddOnShellView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));

        fInterfaceView = new InterfaceView();

        // Build the layout
        BLayoutBuilder::Group<>(this, B_VERTICAL)
                .SetInsets(B_USE_WINDOW_SPACING)

#if ENABLE_PROFILES
                .AddGroup(B_HORIZONTAL, B_USE_SMALL_SPACING)
                        .Add(profilesMenuField)
                        .AddGlue()
                .End()
#endif
                .AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING)
                        .Add(scrollView)
                        .Add(fAddOnShellView)
                .End()

                .Add(showReplicantCheckBox)
                .AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING)
                        .Add(fRevertButton)
                        .AddGlue()
                .End();

        gNetworkWindow = this;

        _ScanInterfaces();
        _ScanAddOns();
        _UpdateRevertButton();

        fListView->Select(0);
        _SelectItem(fListView->ItemAt(0));
                // Call this manually, so that CenterOnScreen() below already
                // knows the final window size.

        // Set size of the list view from its contents
        float width;
        float height;
        fListView->GetPreferredSize(&width, &height);
        width += 2 * be_control_look->DefaultItemSpacing();
        fListView->SetExplicitSize(BSize(width, B_SIZE_UNSET));
        fListView->SetExplicitMinSize(BSize(width, std::min(height, 400.f)));

        CenterOnScreen();

        fSettings.StartMonitoring(this);
        start_watching_network(B_WATCH_NETWORK_INTERFACE_CHANGES
                | B_WATCH_NETWORK_LINK_CHANGES | B_WATCH_NETWORK_WLAN_CHANGES, this);
}


NetworkWindow::~NetworkWindow()
{
        stop_watching_network(this);
        fSettings.StopMonitoring(this);
}


bool
NetworkWindow::QuitRequested()
{
        be_app->PostMessage(B_QUIT_REQUESTED);
        return true;
}


void
NetworkWindow::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case kMsgProfileNew:
                        break;

                case kMsgProfileSelected:
                {
                        const char* path;
                        if (message->FindString("path", &path) != B_OK)
                                break;

                        // TODO!
                        break;
                }

                case kMsgItemSelected:
                {
                        BListItem* listItem = fListView->FullListItemAt(
                                fListView->FullListCurrentSelection());
                        if (listItem == NULL)
                                break;

                        _SelectItem(listItem);
                        break;
                }

                case kMsgRevert:
                {
                        SettingsMap::const_iterator iterator = fSettingsMap.begin();
                        for (; iterator != fSettingsMap.end(); iterator++)
                                iterator->second->Revert();
                        break;
                }

                case kMsgToggleReplicant:
                {
                        _ShowReplicant(
                                message->GetInt32("be:value", B_CONTROL_OFF) == B_CONTROL_ON);
                        break;
                }

                case B_PATH_MONITOR:
                {
                        fSettings.Update(message);
                        break;
                }

                case B_NETWORK_MONITOR:
                        _BroadcastConfigurationUpdate(*message);
                        break;

                case BNetworkSettings::kMsgInterfaceSettingsUpdated:
                case BNetworkSettings::kMsgNetworkSettingsUpdated:
                case BNetworkSettings::kMsgServiceSettingsUpdated:
                        _BroadcastSettingsUpdate(message->what);
                        break;

                case kMsgSettingsItemUpdated:
                        // TODO: update list item
                        _UpdateRevertButton();
                        break;

                default:
                        inherited::MessageReceived(message);
        }
}


void
NetworkWindow::_BuildProfilesMenu(BMenu* menu, int32 what)
{
        char currentProfile[256] = { 0 };

        menu->SetRadioMode(true);

        BDirectory dir("/boot/system/settings/network/profiles");
        if (dir.InitCheck() == B_OK) {
                BEntry entry;

                dir.Rewind();
                while (dir.GetNextEntry(&entry) >= 0) {
                        BPath name;
                        entry.GetPath(&name);

                        if (entry.IsSymLink() &&
                                strcmp("current", name.Leaf()) == 0) {
                                BSymLink symlink(&entry);

                                if (symlink.IsAbsolute())
                                        // oh oh, sorry, wrong symlink...
                                        continue;

                                symlink.ReadLink(currentProfile, sizeof(currentProfile));
                                continue;
                        };

                        if (!entry.IsDirectory())
                                continue;

                        BMessage* message = new BMessage(what);
                        message->AddString("path", name.Path());

                        BMenuItem* item = new BMenuItem(name.Leaf(), message);
                        menu->AddItem(item);
                }
        }

        menu->AddSeparatorItem();
        menu->AddItem(new BMenuItem(B_TRANSLATE("New" B_UTF8_ELLIPSIS),
                new BMessage(kMsgProfileNew)));
        menu->AddItem(new BMenuItem(B_TRANSLATE("Manage" B_UTF8_ELLIPSIS),
                new BMessage(kMsgProfileManage)));

        if (currentProfile[0] != '\0') {
                BMenuItem* item = menu->FindItem(currentProfile);
                if (item != NULL) {
                        // TODO: translate
                        BString label(item->Label());
                        label << " (current)";
                        item->SetLabel(label.String());
                        item->SetMarked(true);
                }
        }
}


void
NetworkWindow::_ScanInterfaces()
{
        // Try existing devices first
        BNetworkRoster& roster = BNetworkRoster::Default();
        BNetworkInterface interface;
        uint32 cookie = 0;

        while (roster.GetNextInterface(&cookie, interface) == B_OK) {
                if ((interface.Flags() & IFF_LOOPBACK) != 0)
                        continue;

                BNetworkDevice device(interface.Name());
                BNetworkInterfaceType type = B_NETWORK_INTERFACE_TYPE_OTHER;

                if (device.IsWireless())
                        type = B_NETWORK_INTERFACE_TYPE_WIFI;
                else if (device.IsEthernet())
                        type = B_NETWORK_INTERFACE_TYPE_ETHERNET;

                InterfaceListItem* item = new InterfaceListItem(interface.Name(), type);
                item->SetExpanded(true);

                fInterfaceItemMap.insert(std::pair<BString, InterfaceListItem*>(
                        BString(interface.Name()), item));
                fListView->AddItem(item);
        }

        // TODO: Then consider those from the settings (for example, for USB)
}


void
NetworkWindow::_ScanAddOns()
{
        BStringList paths;
        BPathFinder::FindPaths(B_FIND_PATH_ADD_ONS_DIRECTORY, "Network Settings",
                paths);

        // Collect add-on paths by name, so that each name will only be
        // loaded once.
        typedef std::map<BString, BPath> PathMap;
        PathMap addOnMap;

        for (int32 i = 0; i < paths.CountStrings(); i++) {
                BDirectory directory(paths.StringAt(i));
                BEntry entry;
                while (directory.GetNextEntry(&entry) == B_OK) {
                        BPath path;
                        if (entry.GetPath(&path) != B_OK)
                                continue;

                        if (addOnMap.find(path.Leaf()) == addOnMap.end())
                                addOnMap.insert(std::pair<BString, BPath>(path.Leaf(), path));
                }
        }

        for (PathMap::const_iterator addOnIterator = addOnMap.begin();
                        addOnIterator != addOnMap.end(); addOnIterator++) {
                const BPath& path = addOnIterator->second;

                image_id image = load_add_on(path.Path());
                if (image < 0) {
                        printf("Failed to load %s addon: %s.\n", path.Path(),
                                strerror(image));
                        continue;
                }

                BNetworkSettingsAddOn* (*instantiateAddOn)(image_id image,
                        BNetworkSettings& settings);

                status_t status = get_image_symbol(image,
                        "instantiate_network_settings_add_on",
                        B_SYMBOL_TYPE_TEXT, (void**)&instantiateAddOn);
                if (status != B_OK) {
                        // No "addon instantiate function" symbol found in this addon
                        printf("No symbol \"instantiate_network_settings_add_on\" found "
                                "in %s addon: not a network setup addon!\n", path.Path());
                        unload_add_on(image);
                        continue;
                }

                BNetworkSettingsAddOn* addOn = instantiateAddOn(image, fSettings);
                if (addOn == NULL) {
                        unload_add_on(image);
                        continue;
                }

                fAddOns.AddItem(addOn);

                // Per interface items
                ItemMap::const_iterator iterator = fInterfaceItemMap.begin();
                for (; iterator != fInterfaceItemMap.end(); iterator++) {
                        const BString& interface = iterator->first;
                        BListItem* interfaceItem = iterator->second;

                        uint32 cookie = 0;
                        while (true) {
                                BNetworkSettingsItem* item = addOn->CreateNextInterfaceItem(
                                        cookie, interface.String());
                                if (item == NULL)
                                        break;

                                fSettingsMap[item->ListItem()] = item;
                                fListView->AddUnder(item->ListItem(), interfaceItem);
                        }
                        fListView->SortItemsUnder(interfaceItem, true,
                                NetworkWindow::_CompareListItems);
                }

                // Generic items
                uint32 cookie = 0;
                while (true) {
                        BNetworkSettingsItem* item = addOn->CreateNextItem(cookie);
                        if (item == NULL)
                                break;

                        fSettingsMap[item->ListItem()] = item;
                        fListView->AddUnder(item->ListItem(),
                                _ListItemFor(item->Type()));
                }

                _SortItemsUnder(fServicesItem);
                _SortItemsUnder(fOtherItem);
        }

        fListView->SortItemsUnder(NULL, true,
                NetworkWindow::_CompareTopLevelListItems);
}


BNetworkSettingsItem*
NetworkWindow::_SettingsItemFor(BListItem* item)
{
        SettingsMap::const_iterator found = fSettingsMap.find(item);
        if (found != fSettingsMap.end())
                return found->second;

        return NULL;
}


void
NetworkWindow::_SortItemsUnder(BListItem* item)
{
        if (item != NULL)
                fListView->SortItemsUnder(item, true, NetworkWindow::_CompareListItems);
}


BListItem*
NetworkWindow::_ListItemFor(BNetworkSettingsType type)
{
        switch (type) {
                case B_NETWORK_SETTINGS_TYPE_SERVICE:
                        if (fServicesItem == NULL)
                                fServicesItem = _CreateItem(B_TRANSLATE("Services"));
                        return fServicesItem;

                case B_NETWORK_SETTINGS_TYPE_OTHER:
                        if (fOtherItem == NULL)
                                fOtherItem = _CreateItem(B_TRANSLATE("Other"));
                        return fOtherItem;

                default:
                        return NULL;
        }
}


BListItem*
NetworkWindow::_CreateItem(const char* label)
{
        BListItem* item = new TitleItem(label);
        item->SetExpanded(true);
        fListView->AddItem(item);
        return item;
}


void
NetworkWindow::_SelectItem(BListItem* listItem)
{
        while (fAddOnShellView->CountChildren() > 0)
                fAddOnShellView->ChildAt(0)->RemoveSelf();

        BView* nextView = NULL;

        BNetworkSettingsItem* item = _SettingsItemFor(listItem);
        if (item != NULL) {
                nextView = item->View();
        } else {
                InterfaceListItem* item = dynamic_cast<InterfaceListItem*>(
                        listItem);
                if (item != NULL) {
                        fInterfaceView->SetTo(item->Name());
                        nextView = fInterfaceView;
                }
        }

        if (nextView != NULL)
                fAddOnShellView->AddChild(nextView);
}


void
NetworkWindow::_BroadcastSettingsUpdate(uint32 type)
{
        for (int32 index = 0; index < fListView->FullListCountItems(); index++) {
                BNetworkSettingsListener* listener
                        = dynamic_cast<BNetworkSettingsListener*>(
                                fListView->FullListItemAt(index));
                if (listener != NULL)
                        listener->SettingsUpdated(type);
        }

        SettingsMap::const_iterator iterator = fSettingsMap.begin();
        for (; iterator != fSettingsMap.end(); iterator++)
                iterator->second->SettingsUpdated(type);

        _UpdateRevertButton();
}


void
NetworkWindow::_BroadcastConfigurationUpdate(const BMessage& message)
{
        for (int32 index = 0; index < fListView->FullListCountItems(); index++) {
                BNetworkConfigurationListener* listener
                        = dynamic_cast<BNetworkConfigurationListener*>(
                                fListView->FullListItemAt(index));
                if (listener != NULL)
                        listener->ConfigurationUpdated(message);
        }

        SettingsMap::const_iterator iterator = fSettingsMap.begin();
        for (; iterator != fSettingsMap.end(); iterator++)
                iterator->second->ConfigurationUpdated(message);

        // TODO: improve invalidated region to the one that matters
        fListView->Invalidate();
        _UpdateRevertButton();
}


void
NetworkWindow::_UpdateRevertButton()
{
        bool enabled = false;
        SettingsMap::const_iterator iterator = fSettingsMap.begin();
        for (; iterator != fSettingsMap.end(); iterator++) {
                if (iterator->second->IsRevertable()) {
                        enabled = true;
                        break;
                }
        }

        fRevertButton->SetEnabled(enabled);
}


void
NetworkWindow::_ShowReplicant(bool show)
{
        if (show) {
                const char* argv[] = {"--deskbar", NULL};

                status_t status = be_roster->Launch(kNetworkStatusSignature, 1, argv);
                if (status != B_OK && status != B_ALREADY_RUNNING) {
                        BString errorMessage;
                        errorMessage.SetToFormat(
                                B_TRANSLATE("Installing NetworkStatus in Deskbar failed: %s"),
                                strerror(status));
                        BAlert* alert = new BAlert(B_TRANSLATE("launch error"),
                                errorMessage, B_TRANSLATE("OK"));
                        alert->Go(NULL);
                }
        } else {
                BDeskbar deskbar;
                deskbar.RemoveItem("NetworkStatus");
        }
}


bool
NetworkWindow::_IsReplicantInstalled()
{
        BDeskbar deskbar;
        return deskbar.HasItem("NetworkStatus");
}


/*static*/ const char*
NetworkWindow::_ItemName(const BListItem* item)
{
        if (const BNetworkInterfaceListItem* listItem = dynamic_cast<
                        const BNetworkInterfaceListItem*>(item))
                return listItem->Label();

        if (const ServiceListItem* listItem = dynamic_cast<
                        const ServiceListItem*>(item))
                return listItem->Label();

        if (const BStringItem* stringItem = dynamic_cast<const BStringItem*>(item))
                return stringItem->Text();

        return NULL;
}


/*static*/ int
NetworkWindow::_CompareTopLevelListItems(const BListItem* a, const BListItem* b)
{
        if (a == b)
                return 0;

        if (const InterfaceListItem* itemA
                        = dynamic_cast<const InterfaceListItem*>(a)) {
                if (const InterfaceListItem* itemB
                                = dynamic_cast<const InterfaceListItem*>(b)) {
                        return strcasecmp(itemA->Name(), itemB->Name());
                }
                return -1;
        } else if (dynamic_cast<const InterfaceListItem*>(b) != NULL)
                return 1;
/*
        if (a == fDialUpItem)
                return -1;
        if (b == fDialUpItem)
                return 1;

        if (a == fServicesItem)
                return -1;
        if (b == fServicesItem)
                return 1;
*/
        return _CompareListItems(a, b);
}


/*static*/ int
NetworkWindow::_CompareListItems(const BListItem* a, const BListItem* b)
{
        if (a == b)
                return 0;

        const char* nameA = _ItemName(a);
        const char* nameB = _ItemName(b);

        if (nameA != NULL && nameB != NULL)
                return strcasecmp(nameA, nameB);
        if (nameA != NULL)
                return 1;
        if (nameB != NULL)
                return -1;

        return (addr_t)a > (addr_t)b ? 1 : -1;
}