/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/Preferences.h"
#include "mozilla/widget/GSettings.h"
#include "nsAppRunner.h"
#include "nsCOMPtr.h"
#include "nsGNOMEShellService.h"
#include "nsShellService.h"
#include "nsIFile.h"
#include "nsIProperties.h"
#include "nsDirectoryServiceDefs.h"
#include "prenv.h"
#include "nsString.h"
#include "nsIGIOService.h"
#include "nsIStringBundle.h"
#include "nsServiceManagerUtils.h"
#include "nsIImageLoadingContent.h"
#include "nsIINIParser.h"
#include "imgIRequest.h"
#include "imgIContainer.h"
#include "mozilla/Components.h"
#include "mozilla/GRefPtr.h"
#include "mozilla/GUniquePtr.h"
#include "mozilla/WidgetUtilsGtk.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Promise.h"
#include "nsImageToPixbuf.h"
#include "nsXULAppAPI.h"

#ifdef MOZ_ENABLE_DBUS
#  include "nsWindow.h"
#  include "WidgetUtils.h"
#  include "mozilla/widget/AsyncDBus.h"
#endif

#include <glib.h>
#include <gio/gio.h>
#include <gdk/gdk.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <stdlib.h>

using namespace mozilla;

struct ProtocolAssociation {
  const char* name;
  bool essential;
};

struct MimeTypeAssociation {
  const char* mimeType;
  const char* extensions;
};

static const ProtocolAssociation appProtocols[] = {
    // clang-format off
  { "http",   true     },
  { "https",  true     },
  { "chrome", false }
    // clang-format on
};

static const MimeTypeAssociation appTypes[] = {
    // clang-format off
  { "text/html",             "htm html shtml" },
  { "application/xhtml+xml", "xhtml xht"      }
    // clang-format on
};

#define kDesktopBGSchema "org.gnome.desktop.background"_ns
#define kDesktopColorGSKey "primary-color"_ns

nsresult nsGNOMEShellService::Init() {
  nsresult rv;

#ifdef MOZ_ENABLE_DBUS
  if (widget::IsGnomeDesktopEnvironment() &&
      Preferences::GetBool("browser.gnome-search-provider.enabled", false)) {
    mSearchProvider.Startup();
  }
#endif

  // Check G_BROKEN_FILENAMES.  If it's set, then filenames in glib use
  // the locale encoding.  If it's not set, they use UTF-8.
  mUseLocaleFilenames = PR_GetEnv("G_BROKEN_FILENAMES") != nullptr;

  if (GetAppPathFromLauncher()) return NS_OK;

  nsCOMPtr<nsIProperties> dirSvc(
      do_GetService("@mozilla.org/file/directory_service;1"));
  NS_ENSURE_TRUE(dirSvc, NS_ERROR_NOT_AVAILABLE);

  nsCOMPtr<nsIFile> appPath;
  rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile),
                   getter_AddRefs(appPath));
  NS_ENSURE_SUCCESS(rv, rv);

  return appPath->GetNativePath(mAppPath);
}

NS_IMPL_ISUPPORTS(nsGNOMEShellService, nsIGNOMEShellService, nsIShellService,
                  nsIToolkitShellService)

bool nsGNOMEShellService::GetAppPathFromLauncher() {
  gchar* tmp;

  const char* launcher = PR_GetEnv("MOZ_APP_LAUNCHER");
  if (!launcher) return false;

  if (g_path_is_absolute(launcher)) {
    mAppPath = launcher;
    tmp = g_path_get_basename(launcher);
    gchar* fullpath = g_find_program_in_path(tmp);
    if (fullpath && mAppPath.Equals(fullpath)) mAppIsInPath = true;
    g_free(fullpath);
  } else {
    tmp = g_find_program_in_path(launcher);
    if (!tmp) return false;
    mAppPath = tmp;
    mAppIsInPath = true;
  }

  g_free(tmp);
  return true;
}

bool nsGNOMEShellService::KeyMatchesAppName(const char* aKeyValue) const {
  gchar* commandPath;
  if (mUseLocaleFilenames) {
    gchar* nativePath =
        g_filename_from_utf8(aKeyValue, -1, nullptr, nullptr, nullptr);
    if (!nativePath) {
      NS_ERROR("Error converting path to filesystem encoding");
      return false;
    }

    commandPath = g_find_program_in_path(nativePath);
    g_free(nativePath);
  } else {
    commandPath = g_find_program_in_path(aKeyValue);
  }

  if (!commandPath) return false;

  bool matches = mAppPath.Equals(commandPath);
  g_free(commandPath);
  return matches;
}

bool nsGNOMEShellService::CheckHandlerMatchesAppName(
    const nsACString& handler) const {
  gint argc;
  gchar** argv;
  nsAutoCString command(handler);

  // The string will be something of the form: [/path/to/]browser "%s"
  // We want to remove all of the parameters and get just the binary name.

  if (g_shell_parse_argv(command.get(), &argc, &argv, nullptr) && argc > 0) {
    command.Assign(argv[0]);
    g_strfreev(argv);
  }

  if (!KeyMatchesAppName(command.get()))
    return false;  // the handler is set to another app

  return true;
}

NS_IMETHODIMP
nsGNOMEShellService::IsDefaultBrowser(bool aForAllTypes,
                                      bool* aIsDefaultBrowser) {
  *aIsDefaultBrowser = false;

  if (widget::IsRunningUnderSnap()) {
    const gchar* argv[] = {"xdg-settings", "check", "default-web-browser",
                           (MOZ_APP_NAME ".desktop"), nullptr};
    GSpawnFlags flags = static_cast<GSpawnFlags>(G_SPAWN_SEARCH_PATH |
                                                 G_SPAWN_STDERR_TO_DEV_NULL);
    gchar* output = nullptr;
    gint exit_status = 0;
    if (!g_spawn_sync(nullptr, (gchar**)argv, nullptr, flags, nullptr, nullptr,
                      &output, nullptr, &exit_status, nullptr)) {
      return NS_OK;
    }
    if (exit_status != 0) {
      g_free(output);
      return NS_OK;
    }
    if (strcmp(output, "yes\n") == 0) {
      *aIsDefaultBrowser = true;
    }
    g_free(output);
    return NS_OK;
  }

  nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
  nsAutoCString handler;
  nsCOMPtr<nsIGIOMimeApp> gioApp;

  for (auto appProtocol : appProtocols) {
    if (!appProtocol.essential) continue;

    if (!IsDefaultForSchemeHelper(nsDependentCString(appProtocol.name),
                                  giovfs)) {
      return NS_OK;
    }
  }

  *aIsDefaultBrowser = true;

  return NS_OK;
}

bool nsGNOMEShellService::IsDefaultForSchemeHelper(
    const nsACString& aScheme, nsIGIOService* giovfs) const {
  nsCOMPtr<nsIGIOService> gioService;
  if (!giovfs) {
    gioService = do_GetService(NS_GIOSERVICE_CONTRACTID);
    giovfs = gioService.get();
  }

  if (!giovfs) {
    return false;
  }

  nsCOMPtr<nsIGIOMimeApp> gioApp;
  nsCOMPtr<nsIHandlerApp> handlerApp;
  giovfs->GetAppForURIScheme(aScheme, getter_AddRefs(handlerApp));
  gioApp = do_QueryInterface(handlerApp);
  if (!gioApp) {
    return false;
  }

  nsAutoCString handler;
  gioApp->GetCommand(handler);
  return CheckHandlerMatchesAppName(handler);
}

NS_IMETHODIMP
nsGNOMEShellService::IsDefaultForScheme(const nsACString& aScheme,
                                        bool* aIsDefaultBrowser) {
  *aIsDefaultBrowser = IsDefaultForSchemeHelper(aScheme, nullptr);
  return NS_OK;
}

NS_IMETHODIMP
nsGNOMEShellService::SetDefaultBrowser(bool aForAllUsers) {
#ifdef DEBUG
  if (aForAllUsers)
    NS_WARNING(
        "Setting the default browser for all users is not yet supported");
#endif

  if (widget::IsRunningUnderSnap()) {
    const gchar* argv[] = {"xdg-settings", "set", "default-web-browser",
                           (MOZ_APP_NAME ".desktop"), nullptr};
    GSpawnFlags flags = static_cast<GSpawnFlags>(G_SPAWN_SEARCH_PATH |
                                                 G_SPAWN_STDOUT_TO_DEV_NULL |
                                                 G_SPAWN_STDERR_TO_DEV_NULL);
    g_spawn_sync(nullptr, (gchar**)argv, nullptr, flags, nullptr, nullptr,
                 nullptr, nullptr, nullptr, nullptr);
    return NS_OK;
  }

  nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
  if (giovfs) {
    nsresult rv;
    nsCOMPtr<nsIStringBundleService> bundleService =
        components::StringBundle::Service(&rv);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsIStringBundle> brandBundle;
    rv = bundleService->CreateBundle(SHELL_BRAND_PROPERTIES_URI,
                                     getter_AddRefs(brandBundle));
    NS_ENSURE_SUCCESS(rv, rv);

    nsAutoString brandShortName;
    brandBundle->GetStringFromName("brandShortName", brandShortName);

    // use brandShortName as the application id.
    NS_ConvertUTF16toUTF8 id(brandShortName);
    nsCOMPtr<nsIGIOMimeApp> appInfo;
    rv = giovfs->FindAppFromCommand(mAppPath, getter_AddRefs(appInfo));
    if (NS_FAILED(rv)) {
      // Application was not found in the list of installed applications
      // provided by OS. Fallback to create appInfo from command and name.
      rv = giovfs->CreateAppFromCommand(mAppPath, id, getter_AddRefs(appInfo));
      NS_ENSURE_SUCCESS(rv, rv);
    }

    // set handler for the protocols
    for (auto appProtocol : appProtocols) {
      appInfo->SetAsDefaultForURIScheme(nsDependentCString(appProtocol.name));
    }

    // set handler for .html and xhtml files and MIME types:
    // Add mime types for html, xhtml extension and set app to just created
    // appinfo.
    for (auto appType : appTypes) {
      appInfo->SetAsDefaultForMimeType(nsDependentCString(appType.mimeType));
      appInfo->SetAsDefaultForFileExtensions(
          nsDependentCString(appType.extensions));
    }
  }

  nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
  if (prefs) {
    (void)prefs->SetBoolPref(PREF_CHECKDEFAULTBROWSER, true);
    // Reset the number of times the dialog should be shown
    // before it is silenced.
    (void)prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, 0);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsGNOMEShellService::GetCanSetDesktopBackground(bool* aResult) {
  // setting desktop background is currently only supported
  // for Gnome or desktops using the same GSettings keys
  if (widget::IsGnomeDesktopEnvironment()) {
    *aResult = true;
    return NS_OK;
  }

  *aResult = !!getenv("GNOME_DESKTOP_SESSION_ID");
  return NS_OK;
}

static nsresult WriteImage(const nsCString& aPath, imgIContainer* aImage) {
  RefPtr<GdkPixbuf> pixbuf = nsImageToPixbuf::ImageToPixbuf(aImage);
  if (!pixbuf) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  gboolean res = gdk_pixbuf_save(pixbuf, aPath.get(), "png", nullptr, nullptr);
  return res ? NS_OK : NS_ERROR_FAILURE;
}

NS_IMETHODIMP
nsGNOMEShellService::SetDesktopBackground(dom::Element* aElement,
                                          int32_t aPosition,
                                          const nsACString& aImageName) {
  nsCOMPtr<nsIImageLoadingContent> imageContent = do_QueryInterface(aElement);
  if (!imageContent) {
    return NS_ERROR_FAILURE;
  }

  // get the image container
  nsCOMPtr<imgIRequest> request;
  imageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
                           getter_AddRefs(request));
  if (!request) {
    return NS_ERROR_FAILURE;
  }
  nsCOMPtr<imgIContainer> container;
  request->GetImage(getter_AddRefs(container));
  if (!container) {
    return NS_ERROR_FAILURE;
  }

  // Set desktop wallpaper filling style
  nsAutoCString options;
  if (aPosition == BACKGROUND_TILE)
    options.AssignLiteral("wallpaper");
  else if (aPosition == BACKGROUND_STRETCH)
    options.AssignLiteral("stretched");
  else if (aPosition == BACKGROUND_FILL)
    options.AssignLiteral("zoom");
  else if (aPosition == BACKGROUND_FIT)
    options.AssignLiteral("scaled");
  else if (aPosition == BACKGROUND_SPAN)
    options.AssignLiteral("spanned");
  else
    options.AssignLiteral("centered");

  // Write the background file to the home directory.
  nsAutoCString filePath(PR_GetEnv("HOME"));
  nsAutoString brandName;

  // get the product brand name from localized strings
  if (nsCOMPtr<nsIStringBundleService> bundleService =
          components::StringBundle::Service()) {
    nsCOMPtr<nsIStringBundle> brandBundle;
    bundleService->CreateBundle(SHELL_BRAND_PROPERTIES_URI,
                                getter_AddRefs(brandBundle));
    if (bundleService) {
      brandBundle->GetStringFromName("brandShortName", brandName);
    }
  }

  // build the file name
  filePath.Append('/');
  filePath.Append(NS_ConvertUTF16toUTF8(brandName));
  filePath.AppendLiteral("_wallpaper.png");

  // write the image to a file in the home dir
  MOZ_TRY(WriteImage(filePath, container));

  widget::GSettings::Collection bgSettings(kDesktopBGSchema);
  if (!bgSettings) {
    return NS_ERROR_FAILURE;
  }

  GUniquePtr<gchar> fileURI(
      g_filename_to_uri(filePath.get(), nullptr, nullptr));
  if (!fileURI) {
    return NS_ERROR_FAILURE;
  }

  bgSettings.SetString("picture-options"_ns, options);
  bgSettings.SetString("picture-uri"_ns, nsDependentCString(fileURI.get()));
  bgSettings.SetString("picture-uri-dark"_ns,
                       nsDependentCString(fileURI.get()));
  return NS_OK;
}

#define COLOR_16_TO_8_BIT(_c) ((_c) >> 8)
#define COLOR_8_TO_16_BIT(_c) ((_c) << 8 | (_c))

NS_IMETHODIMP
nsGNOMEShellService::GetDesktopBackgroundColor(uint32_t* aColor) {
  nsAutoCString background;
  widget::GSettings::GetString(kDesktopBGSchema, kDesktopColorGSKey,
                               background);
  if (background.IsEmpty()) {
    *aColor = 0;
    return NS_OK;
  }

  GdkColor color;
  gboolean success = gdk_color_parse(background.get(), &color);

  NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);

  *aColor = COLOR_16_TO_8_BIT(color.red) << 16 |
            COLOR_16_TO_8_BIT(color.green) << 8 | COLOR_16_TO_8_BIT(color.blue);
  return NS_OK;
}

static void ColorToCString(uint32_t aColor, nsCString& aResult) {
  // The #rrrrggggbbbb format is used to match gdk_color_to_string()
  aResult.SetLength(13);
  char* buf = aResult.BeginWriting();
  if (!buf) return;

  uint16_t red = COLOR_8_TO_16_BIT((aColor >> 16) & 0xff);
  uint16_t green = COLOR_8_TO_16_BIT((aColor >> 8) & 0xff);
  uint16_t blue = COLOR_8_TO_16_BIT(aColor & 0xff);

  snprintf(buf, 14, "#%04x%04x%04x", red, green, blue);
}

NS_IMETHODIMP
nsGNOMEShellService::SetDesktopBackgroundColor(uint32_t aColor) {
  NS_ASSERTION(aColor <= 0xffffff, "aColor has extra bits");
  nsAutoCString colorString;
  ColorToCString(aColor, colorString);

  widget::GSettings::Collection bgSettings(kDesktopBGSchema);
  if (bgSettings) {
    bgSettings.SetString(kDesktopColorGSKey, colorString);
    return NS_OK;
  }

  return NS_ERROR_FAILURE;
}

NS_IMETHODIMP
nsGNOMEShellService::GetGSettingsString(const nsACString& aSchema,
                                        const nsACString& aKey,
                                        nsACString& aResult) {
  widget::GSettings::GetString(PromiseFlatCString(aSchema),
                               PromiseFlatCString(aKey), aResult);
  return NS_OK;
}

NS_IMETHODIMP
nsGNOMEShellService::SetGSettingsString(const nsACString& aSchema,
                                        const nsACString& aKey,
                                        const nsACString& aValue) {
  widget::GSettings::Collection settings(PromiseFlatCString(aSchema));
  if (!settings) {
    return NS_ERROR_FAILURE;
  }
  if (!settings.SetString(PromiseFlatCString(aKey),
                          PromiseFlatCString(aValue))) {
    return NS_ERROR_FAILURE;
  }
  return NS_OK;
}

NS_IMETHODIMP nsGNOMEShellService::GetArgv0(nsACString& output) {
  output.Assign(gArgc <= 0 ? "" : gArgv[0]);
  return NS_OK;
}

NS_IMETHODIMP nsGNOMEShellService::GetGlibPrgname(nsACString& output) {
  output.Assign(g_get_prgname());
  return NS_OK;
}

NS_IMETHODIMP nsGNOMEShellService::GetDesktopEntryStatus(
    const nsACString& aDesktopId,
    nsIGNOMEShellService::DesktopEntryStatus* aResult) {
  NS_ENSURE_ARG_POINTER(aResult);

  RefPtr<GDesktopAppInfo> appinfo =
      dont_AddRef(g_desktop_app_info_new(PromiseFlatCString(aDesktopId).get()));
  if (!appinfo) {
    *aResult = nsIGNOMEShellService::DESKTOP_ENTRY_ABSENT;
  } else if (g_app_info_should_show(G_APP_INFO(appinfo.get()))) {
    *aResult = nsIGNOMEShellService::DESKTOP_ENTRY_VISIBLE;
  } else {
    *aResult = nsIGNOMEShellService::DESKTOP_ENTRY_INVISIBLE;
  }

  return NS_OK;
}

using GIconPromise = MozPromise<RefPtr<GIcon>, GUniquePtr<GError>, true>;
class AsyncGIconReader {
 public:
  // This may be freed by the background thread.
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncGIconReader);

  explicit AsyncGIconReader(const nsCString& aPath) { mPath.Assign(aPath); }

  RefPtr<GIconPromise> Start() {
    RefPtr<GIconPromise> promise = mHolder.Ensure(__func__);
    MOZ_ASSERT(!mHolder.IsEmpty());

    NS_DispatchBackgroundTask(
        NS_NewRunnableFunction(
            __func__,
            [this, inst = RefPtr{this}] {
              char* content = nullptr;
              size_t length = 0;
              GUniquePtr<GError> error;
              if (!g_file_get_contents(mPath.get(), &content, &length,
                                       getter_Transfers(error))) {
                mHolder.Reject(std::move(error), __func__);
                return;
              }

              GBytes* bytes = g_bytes_new_take(content, length);
              // Note that this doesn't decode the image and only puts the
              // bytes into a new container that serializes over D-Bus.
              RefPtr<GIcon> icon = dont_AddRef(g_bytes_icon_new(bytes));
              g_clear_pointer(&bytes, g_bytes_unref);

              mHolder.Resolve(std::move(icon), __func__);
            }),
        NS_DISPATCH_EVENT_MAY_BLOCK);

    return promise.forget();
  }

 private:
  MozPromiseHolder<GIconPromise> mHolder;
  nsCString mPath;

  ~AsyncGIconReader() = default;
};

#ifdef MOZ_ENABLE_DBUS
static RefPtr<widget::DBusProxyPromise> CreateDynamicLauncherProxy() {
  return widget::CreateDBusProxyForBus(
      G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
      "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
      "org.freedesktop.portal.DynamicLauncher");
}
#endif

NS_IMETHODIMP nsGNOMEShellService::RequestInstallDynamicLauncher(
    const nsACString& aEntryId, nsIINIParserWriter* aDesktopEntry,
    mozIDOMWindowProxy* aWindow, JSContext* aCx, dom::Promise** aPromise) {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());

#ifdef MOZ_ENABLE_DBUS
  ErrorResult rv;
  RefPtr<dom::Promise> promise =
      dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
  if (MOZ_UNLIKELY(rv.Failed())) {
    return rv.StealNSResult();
  }

  nsWindow* window = nullptr;
  if (aWindow) {
    auto* parent = nsPIDOMWindowOuter::From(aWindow);
    RefPtr<nsIWidget> widget =
        mozilla::widget::WidgetUtils::DOMWindowToWidget(parent);
    NS_ENSURE_TRUE(widget, NS_ERROR_FAILURE);

    window = nsWindow::FromWidget(widget);
  }

  nsCOMPtr<nsIINIParser> entryINI(do_QueryInterface(aDesktopEntry));
  NS_ENSURE_ARG(entryINI);

  nsCString entryName;
  nsCString entryIcon;
  nsCString entryContent;
  MOZ_TRY(entryINI->GetString("Desktop Entry"_ns, "Name"_ns, entryName));
  MOZ_TRY(entryINI->GetString("Desktop Entry"_ns, "Icon"_ns, entryIcon));
  MOZ_TRY(aDesktopEntry->WriteToString(entryContent));

  auto* target = GetCurrentSerialEventTarget();

  // MozPromise doesn't seem to support ::All with different types, so make one
  // type to contain them all.
  using DynamicLauncherPromise =
      MozPromise<Variant<RefPtr<GDBusProxy>, RefPtr<GIcon>, nsCString>,
                 GUniquePtr<GError>, true>;

  RefPtr<DynamicLauncherPromise> dbusProxyPromise =
      CreateDynamicLauncherProxy()->Map(
          target, __func__,
          [](RefPtr<GDBusProxy>&& proxy)
              -> DynamicLauncherPromise::ResolveValueType {
            return AsVariant(std::move(proxy));
          });

  RefPtr<AsyncGIconReader> reader = new AsyncGIconReader(entryIcon);
  RefPtr<DynamicLauncherPromise> iconPromise = reader->Start()->Map(
      target, __func__,
      [](RefPtr<GIcon>&& icon) -> DynamicLauncherPromise::ResolveValueType {
        return AsVariant(std::move(icon));
      });

  RefPtr<DynamicLauncherPromise> exportPromise =
      (window
           ? window->ExportHandle()
           : nsWindow::ExportHandlePromise::CreateAndResolve(""_ns, __func__))
          ->Then(
              GetCurrentSerialEventTarget(), __func__,
              [](nsWindow::ExportHandlePromise::ResolveOrRejectValue&& aValue) {
                if (aValue.IsResolve()) {
                  return DynamicLauncherPromise::CreateAndResolve(
                      AsVariant(aValue.ResolveValue()), __func__);
                }

                NS_WARNING("Failed to export window for DynamicLauncher");
                return DynamicLauncherPromise::CreateAndResolve(
                    AsVariant(EmptyCString()), __func__);
              });

  nsTArray<RefPtr<DynamicLauncherPromise>> promises = {
      RefPtr(dbusProxyPromise), RefPtr(iconPromise), RefPtr(exportPromise)};
  DynamicLauncherPromise::All(target, promises)
      ->Then(
          target, __func__,
          [target = nsCOMPtr{target}, entryId = nsCString(aEntryId),
           entryContent = std::move(entryContent),
           entryName = std::move(entryName)](
              DynamicLauncherPromise::AllPromiseType::ResolveOrRejectValue&&
                  aValue) {
            if (aValue.IsReject()) {
              return widget::DBusCallPromise::AllPromiseType::CreateAndReject(
                  std::move(aValue.RejectValue()), __func__);
            }

            auto values = aValue.ResolveValue();
            RefPtr<GDBusProxy> proxy =
                std::move(values[0].template as<RefPtr<GDBusProxy>>());
            RefPtr<GIcon> icon = values[1].template as<RefPtr<GIcon>>();
            nsCString exportedWindow = values[2].template as<nsCString>();

            nsAutoCString token;
            widget::MakePortalRequestToken("DynamicLauncher"_ns, token);

            nsAutoCString requestPath;
            widget::GetPortalRequestPath(proxy.get(), token, requestPath);

            auto holder =
                std::make_shared<MozPromiseHolder<widget::DBusCallPromise>>();
            RefPtr<widget::DBusCallPromise> installPromise =
                holder->Ensure(__func__);

            GVariantDict options;
            g_variant_dict_init(&options, nullptr);
            g_variant_dict_insert(&options, "handle_token", "s", token.get());

            RefPtr<GVariant> args = dont_AddRef(g_variant_ref_sink(
                g_variant_new("(ssv@a{sv})",        //
                              exportedWindow.get(), /* parent window */
                              entryName.get(),      /* name of entry */
                              g_icon_serialize(G_ICON(icon.get())), /* icon */
                              g_variant_dict_end(&options)))); /* options */

            auto subscription = std::make_shared<unsigned>(0);
            *subscription = widget::OnDBusPortalResponse(
                G_DBUS_PROXY(proxy.get()), token,
                [target = nsCOMPtr{target}, entryId = std::move(entryId),
                 entryContent, proxy, holder, subscription](GVariant* variant) {
                  *subscription = 0;

                  unsigned response = 2;  // '2' indicates 'other error'
                  RefPtr<GVariant> options = nullptr;
                  g_variant_get(variant, "(u@a{sv})", &response,
                                options.StartAssignment());
                  if (response != 0) {
                    holder->Reject(GUniquePtr<GError>(g_error_new(
                                       G_IO_ERROR, G_IO_ERROR_FAILED,
                                       "Response was non-zero")),
                                   __func__);
                    return;
                  }

                  const char* responseToken;
                  if (!g_variant_lookup(options, "token", "&s",
                                        &responseToken)) {
                    holder->Reject(
                        GUniquePtr<GError>(g_error_new(
                            G_IO_ERROR, G_IO_ERROR_FAILED,
                            "No token was provided from the portal")),
                        __func__);
                    return;
                  }

                  nsPrintfCString entryIdWithExtension("%s.desktop",
                                                       entryId.get());
                  RefPtr<GVariant> args = dont_AddRef(g_variant_ref_sink(
                      g_variant_new("(sssa{sv})", responseToken,
                                    entryIdWithExtension.get(),
                                    entryContent.get(), nullptr)));

                  widget::DBusProxyCall(proxy.get(), "Install", args,
                                        G_DBUS_CALL_FLAGS_NONE, -1)
                      ->Then(target, __func__,
                             [holder = std::move(holder)](
                                 widget::DBusCallPromise::ResolveOrRejectValue&&
                                     aValue) {
                               holder->ResolveOrReject(std::move(aValue),
                                                       __func__);
                             });
                });

            RefPtr<widget::DBusCallPromise> prepareInstallPromise =
                widget::DBusProxyCall(G_DBUS_PROXY(proxy.get()),
                                      "PrepareInstall", args,
                                      G_DBUS_CALL_FLAGS_NONE, -1)
                    ->MapErr(target, __func__,
                             [subscriptionptr = std::move(subscription), proxy,
                              holder](GUniquePtr<GError>&& err) {
                               if (*subscriptionptr) {
                                 g_dbus_connection_signal_unsubscribe(
                                     g_dbus_proxy_get_connection(proxy.get()),
                                     *subscriptionptr);
                               }

                               holder->Reject(
                                   GUniquePtr<GError>(g_error_copy(err.get())),
                                   __func__);
                               return std::move(err);
                             });

            nsTArray<RefPtr<widget::DBusCallPromise>> promises = {
                prepareInstallPromise, installPromise};
            return widget::DBusCallPromise::All(target, promises);
          })
      ->Then(target, __func__,
             [promise](
                 widget::DBusCallPromise::AllPromiseType::ResolveOrRejectValue&&
                     aValue) {
               if (aValue.IsReject()) {
                 nsDependentCString message(aValue.RejectValue()->message);
                 promise->MaybeRejectWithOperationError(message);
               } else {
                 promise->MaybeResolveWithUndefined();
               }
             });

  promise.forget(aPromise);
  return NS_OK;
#else
  return NS_ERROR_FAILURE;
#endif
}

NS_IMETHODIMP nsGNOMEShellService::RequestUninstallDynamicLauncher(
    const nsACString& aEntryId, JSContext* aCx, dom::Promise** aPromise) {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
#ifdef MOZ_ENABLE_DBUS
  ErrorResult rv;
  RefPtr<dom::Promise> promise =
      dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
  if (MOZ_UNLIKELY(rv.Failed())) {
    return rv.StealNSResult();
  }

  nsCString entryId = PromiseFlatCString(aEntryId);
  nsPrintfCString entryIdWithExtension("%s.desktop", entryId.get());

  CreateDynamicLauncherProxy()
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [entryIdWithExtension](const RefPtr<GDBusProxy>& aProxy) {
            RefPtr<GVariant> args =
                dont_AddRef(g_variant_ref_sink(g_variant_new(
                    "(sa{sv})", entryIdWithExtension.get(), nullptr)));
            return widget::DBusProxyCall(aProxy.get(), "Uninstall", args,
                                         G_DBUS_CALL_FLAGS_NONE, -1);
          },
          [](GUniquePtr<GError>&& error) {
            return widget::DBusCallPromise::CreateAndReject(std::move(error),
                                                            __func__);
          })
      ->Then(GetCurrentSerialEventTarget(), __func__,
             [promise](widget::DBusCallPromise::ResolveOrRejectValue&& aValue) {
               if (aValue.IsReject()) {
                 nsDependentCString message(aValue.RejectValue()->message);
                 promise->MaybeRejectWithOperationError(message);
               } else {
                 promise->MaybeResolveWithUndefined();
               }
             });

  promise.forget(aPromise);
  return NS_OK;
#else
  return NS_ERROR_FAILURE;
#endif
}
