From d44f07a0c2cc410414bfd7b338ee071c17422a0a Mon Sep 17 00:00:00 2001
From: Marius Vlad <marius.vlad@collabora.com>
Date: Mon, 4 Dec 2023 18:17:00 +0200
Subject: [PATCH 2/2] display: Add support for agl_shell version 8

Signed-off-by: Marius Vlad <marius.vlad@collabora.com>
---
 shell/wayland/display.cc               | 156 ++++++++++++++++++++++--
 shell/wayland/display.h                |  58 +++++++++
 third_party/agl/protocol/agl-shell.xml | 160 ++++++++++++++++++++++++-
 3 files changed, 366 insertions(+), 8 deletions(-)

diff --git a/shell/wayland/display.cc b/shell/wayland/display.cc
index 3ee814a..aba050a 100644
--- a/shell/wayland/display.cc
+++ b/shell/wayland/display.cc
@@ -220,7 +220,7 @@ void Display::registry_handle_global(void* data,
     if (version >= 2) {
       d->m_agl.shell = static_cast<struct agl_shell*>(
           wl_registry_bind(registry, name, &agl_shell_interface,
-                           std::min(static_cast<uint32_t>(4), version)));
+                           std::min(static_cast<uint32_t>(8), version)));
       agl_shell_add_listener(d->m_agl.shell, &agl_shell_listener, data);
     } else {
       d->m_agl.shell = static_cast<struct agl_shell*>(
@@ -980,6 +980,148 @@ void Display::agl_shell_bound_fail(void* data, struct agl_shell* shell) {
   d->m_agl.bound_ok = false;
 }
 
+void Display::addAppToStack(std::string app_id) {
+  if (app_id == "homescreen")
+    return;
+
+  bool found_app = false;
+  for (auto& i : apps_stack) {
+    if (i == app_id) {
+      found_app = true;
+      break;
+    }
+  }
+
+  if (!found_app) {
+    apps_stack.push_back(app_id);
+  } else {
+    // fixme
+  }
+}
+
+int Display::find_output_by_name(std::string output_name) {
+  int index = 0;
+  for (auto& i : m_all_outputs) {
+    if (i->name == output_name) {
+      return index;
+    }
+    index++;
+  }
+
+  return -1;
+}
+
+void Display::activateApp(std::string app_id) {
+  int default_output_index = 0;
+
+  FML_LOG(INFO) << "got app_id " << app_id;
+
+  // search for a pending application which might have a different output
+  auto iter = pending_app_list.begin();
+  bool found_pending_app = false;
+  while (iter != pending_app_list.end()) {
+    auto app_to_search = iter->first;
+    FML_LOG(INFO) << "searching for " << app_to_search;
+
+    if (app_to_search == app_id) {
+      found_pending_app = true;
+      break;
+    }
+
+    iter++;
+  }
+
+  if (found_pending_app) {
+    auto output_name = iter->second;
+    default_output_index = find_output_by_name(output_name);
+
+    FML_LOG(INFO) << "Found app_id " << app_id << " at all";
+
+    if (default_output_index < 0) {
+      // try with remoting-remote-X which is the streaming
+      std::string new_remote_output = "remoting-" + output_name;
+
+      default_output_index = find_output_by_name(new_remote_output);
+      if (default_output_index < 0) {
+        FML_LOG(INFO) << "Not activating app_id " << app_id << " at all";
+        return;
+      }
+    }
+
+    pending_app_list.erase(iter);
+  }
+
+  FML_LOG(INFO) << "Activating app_id " << app_id << " on output "
+                << default_output_index;
+  agl_shell_activate_app(m_agl.shell, app_id.c_str(),
+                         m_all_outputs[default_output_index]->output);
+  wl_display_flush(m_display);
+}
+
+void Display::deactivateApp(std::string app_id) {
+  for (auto& i : apps_stack) {
+    if (i == app_id) {
+      // remove it from apps_stack
+      apps_stack.remove(i);
+      if (!apps_stack.empty())
+        activateApp(apps_stack.back());
+      break;
+    }
+  }
+}
+
+void Display::processAppStatusEvent(const char* app_id,
+                                    const std::string event_type) {
+  if (!m_agl.shell)
+    return;
+
+  if (event_type == "started") {
+    activateApp(std::string(app_id));
+  } else if (event_type == "terminated") {
+    deactivateApp(std::string(app_id));
+  } else if (event_type == "deactivated") {
+    // not handled
+  }
+}
+
+void Display::agl_shell_app_on_output(void* data,
+                                      struct agl_shell* agl_shell,
+                                      const char* app_id,
+                                      const char* output_name) {
+  auto* d = static_cast<Display*>(data);
+
+  FML_LOG(INFO) << "Gove event app_on_out app_id " << app_id << " output name "
+                << output_name;
+
+  // a couple of use-cases, if there is no app_id in the app_list then it
+  // means this is a request to map the application, from the start to a
+  // different output that the default one. We'd get an
+  // AGL_SHELL_APP_STATE_STARTED which will handle activation.
+  //
+  // if there's an app_id then it means we might have gotten an event to
+  // move the application to another output; so we'd need to process it
+  // by explicitly calling processAppStatusEvent() which would ultimately
+  // activate the application on other output. We'd have to pick-up the
+  // last activated window and activate the default output.
+  //
+  // finally if the outputs are identical probably that's an user-error -
+  // but the compositor won't activate it again, so we don't handle that.
+  std::pair new_pending_app =
+      std::pair(std::string(app_id), std::string(output_name));
+  d->pending_app_list.push_back(new_pending_app);
+
+  auto iter = d->apps_stack.begin();
+  while (iter != d->apps_stack.end()) {
+    if (*iter == std::string(app_id)) {
+      FML_LOG(INFO) << "Gove event to move " << app_id << " to another output "
+                    << output_name;
+      d->processAppStatusEvent(app_id, std::string("started"));
+      break;
+    }
+    iter++;
+  }
+}
+
 void Display::agl_shell_app_state(void* data,
                                   struct agl_shell* /* agl_shell */,
                                   const char* app_id,
@@ -991,12 +1133,7 @@ void Display::agl_shell_app_state(void* data,
       FML_DLOG(INFO) << "Got AGL_SHELL_APP_STATE_STARTED for app_id " << app_id;
 
       if (d->m_agl.shell) {
-        // we always assume the first output advertised by the wl_output
-        // interface
-        unsigned int default_output_index = 0;
-
-        agl_shell_activate_app(d->m_agl.shell, app_id,
-                               d->m_all_outputs[default_output_index]->output);
+        d->processAppStatusEvent(app_id, std::string("started"));
       }
 
       break;
@@ -1007,6 +1144,10 @@ void Display::agl_shell_app_state(void* data,
     case AGL_SHELL_APP_STATE_ACTIVATED:
       FML_DLOG(INFO) << "Got AGL_SHELL_APP_STATE_ACTIVATED for app_id "
                      << app_id;
+      d->addAppToStack(std::string(app_id));
+      break;
+    case AGL_SHELL_APP_STATE_DEACTIVATED:
+      d->processAppStatusEvent(app_id, std::string("deactivated"));
       break;
     default:
       break;
@@ -1017,6 +1158,7 @@ const struct agl_shell_listener Display::agl_shell_listener = {
     .bound_ok = agl_shell_bound_ok,
     .bound_fail = agl_shell_bound_fail,
     .app_state = agl_shell_app_state,
+    .app_on_output = agl_shell_app_on_output,
 };
 
 void Display::ivi_wm_surface_visibility(void* /* data */,
diff --git a/shell/wayland/display.h b/shell/wayland/display.h
index a0756f0..b919047 100644
--- a/shell/wayland/display.h
+++ b/shell/wayland/display.h
@@ -18,6 +18,7 @@
 #pragma once
 
 #include <chrono>
+#include <list>
 #include <memory>
 #include <mutex>
 #include <string>
@@ -271,6 +272,44 @@ class Display {
    */
   std::pair<int32_t, int32_t> GetVideoModeSize(uint32_t index);
 
+  /**
+   * @brief deactivate/hide the application pointed by app_id
+   * @param[in] app_id the app_id
+   * @relation
+   * agl_shell
+   */
+  void deactivateApp(std::string app_id);
+  /**
+   * @brief activate/show the application pointed by app_id
+   * @param[in] app_id the app_id
+   * @relation
+   * agl_shell
+   */
+  void activateApp(std::string app_id);
+  /**
+   * @brief Add app_id to a list of list applications
+   * @param[in] app_id the app_id
+   * @relation
+   * agl_shell
+   */
+  void addAppToStack(std::string app_id);
+  /**
+   * @brief Helper to retrieve the output using its output_name
+   * @param[in] output_name a std::string representing the output
+   * @retval an integer that can used to get the proper output
+   * @relation
+   * agl_sell
+   */
+  int find_output_by_name(std::string output_name);
+  /**
+   * @brief helper to process the application status
+   * @param[in] app_id an array of char
+   * @param[in] event_type a std::string representing the type of event (started/stopped/terminated)
+   * @relation
+   * agl_shell
+   */
+  void processAppStatusEvent(const char* app_id, const std::string event_type);
+
  private:
   std::shared_ptr<Engine> m_flutter_engine;
 
@@ -300,6 +339,9 @@ class Display {
     uint32_t version = 0;
   } m_agl;
 
+  std::list<std::string> apps_stack;
+  std::list<std::pair<const std::string, const std::string>> pending_app_list;
+
   struct ivi_shell {
     struct ivi_application* application = nullptr;
     struct ivi_wm* ivi_wm = nullptr;
@@ -982,6 +1024,22 @@ class Display {
                                   const char* app_id,
                                   uint32_t state);
 
+  /**
+   * @brief AGL app_app_on_output event
+   * @param[in,out] data Data of type Display
+   * @param[in] shell No use
+   * @param[in] app_id the application id for which this event was sent
+   * @param[in] state the state: CREATED/TERMINATED/ACTIVATED/DEACTIVATED
+   * @return void
+   * @relation
+   * wayland, agl-shell
+   * @note Do nothing
+   */
+  static void agl_shell_app_on_output(void* data,
+                                      struct agl_shell* agl_shell,
+                                      const char* app_id,
+                                      const char* output_name);
+
   static const struct agl_shell_listener agl_shell_listener;
 
   /**
diff --git a/third_party/agl/protocol/agl-shell.xml b/third_party/agl/protocol/agl-shell.xml
index bf5ab02..e010a80 100644
--- a/third_party/agl/protocol/agl-shell.xml
+++ b/third_party/agl/protocol/agl-shell.xml
@@ -22,7 +22,7 @@
     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
     DEALINGS IN THE SOFTWARE.
   </copyright>
-  <interface name="agl_shell" version="4">
+  <interface name="agl_shell" version="8">
     <description summary="user interface for Automotive Grade Linux platform">
       Starting with version 2 of the protocol, the client is required to wait
       for the 'bound_ok' or 'bound_fail' events in order to proceed further.
@@ -200,5 +200,163 @@
       <arg name="width" type="int" summary="width of rectangle"/>
       <arg name="height" type="int" summary="height of rectangle"/>
     </request>
+
+    <request name="deactivate_app" since="5">
+      <description summary="de-activate/hide window identified by app_id">
+        Ask the compositor to hide the toplevel window for window
+        management purposes. Depending on the window role, this request
+        will either display the previously active window (or the background
+        in case there's no previously active surface) or temporarily (or
+        until a 'activate_app' is called upon) hide the surface.
+
+        All the surfaces are identifiable by using the app_id, and no actions
+        are taken in case the app_id is not/was not present.
+
+        See xdg_toplevel.set_app_id from the xdg-shell protocol for a
+        description of app_id.
+      </description>
+      <arg name="app_id" type="string"/>
+    </request>
+
+    <request name="set_app_float" since="6">
+      <description summary="set the window identified by app_id as float">
+        Makes the application identified by app_id as floating. If the
+        application's window is already mapped, in a maximized, normal state,
+        it would transition to the float state.
+
+        For applications that want to modify their own state, this request
+        must be done before the initial surface commit in order to take effect.
+
+        If the application is already in floating state, this request wouldn't
+        do anything.
+
+        There's no persistence of this request, once the application terminated
+        you'll to issue this request again for that particular app_id.
+
+        The x, and y values would be initial position of the window where the
+        window surface will be placed.
+
+        See xdg_toplevel.set_app_id from the xdg-shell protocol for a
+        description of app_id.
+      </description>
+      <arg name="app_id" type="string"/>
+      <arg name="x" type="int" summary="x position"/>
+      <arg name="y" type="int" summary="y position"/>
+    </request>
+
+    <request name="set_app_normal" since="6">
+      <description summary="set the window identified by app_id as normally started">
+      Returns the application identified by app_id as it was in the normal state.
+      This is useful to come back from other states to the maximized state, the
+      normal state applications are started.
+      </description>
+      <arg name="app_id" type="string"/>
+    </request>
+
+    <request name="set_app_fullscreen" since="7">
+      <description summary="">
+        Makes the application identified by app_id as fullscreen. If the
+        application's window is already mapped, in a maximized, normal state,
+        it would transition to the fullscreen state.
+
+        For applications that want to modify their own state, this request
+        must be done before the initial surface commit in order to take effect.
+
+        If the application is already in fullscreen state, this request wouldn't
+        do anything.
+
+        There's no persistence of this request, once the application terminated
+        you'll to issue this request again for that particular app_id.
+
+        See xdg_toplevel.set_app_id from the xdg-shell protocol for a
+        description of app_id.
+      </description>
+      <arg name="app_id" type="string"/>
+    </request>
+
+    <request name="set_app_output" since="8">
+      <description summary="Assign an application to a particular output">
+        This would allow the compositor to place an application on a particular
+        output, if that output is indeed available. This can happen before
+        application is started which would make the application start on that
+        particular output. If the application is already started it would
+        move the application to that output.
+
+        There's no persistence of this request, once the application terminated
+        you'll need to issue this request again for that particular app_id.
+
+        See xdg_toplevel.set_app_id from the xdg-shell protocol for a
+        description of app_id.
+      </description>
+      <arg name="app_id" type="string"/>
+      <arg name="output" type="object" interface="wl_output"/>
+    </request>
+
+    <event name="app_on_output" since="8">
+      <description summary="Event sent as a reponse to set_app_output">
+        Clients can use this event to be notified when an application
+        wants to be displayed on a certain output. This event is sent in
+        response to the set_app_output request.
+
+        See xdg_toplevel.set_app_id from the xdg-shell protocol for a
+        description of app_id.
+      </description>
+      <arg name="app_id" type="string"/>
+      <arg name="output_name" type="string"/>
+    </event>
+  </interface>
+
+  <interface name="agl_shell_ext" version="1">
+    <description summary="extended user interface for Automotive Grade Linux platform">
+      This interface allows another client bind to the agl_shell interface,
+      while there's another shell client already present.
+
+      The client should first bind to this interface and then inform the
+      compositor with the 'doas_shell_client' request and it wants to bind to
+      the agl_shell interface. The client is still expected, if using a new
+      version of the agl_shell interface, to wait for the 'bound_ok' and
+      'bound_fail' events before issueing any other requests/events.
+
+      Note that this interface has its limitations, and the compositor would
+      still refuse the act for 'set_panel' or 'set_background' requests
+      of the agl_shell interface if there's already a client that used them.
+
+      Any other requests or events should be delievered and handled as it would
+      a client bound to the agl_shell interface.
+    </description>
+
+    <enum name="doas_shell_client_status">
+      <entry name="success" value="0"/>
+      <entry name="failed" value="1"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroys the factory object">
+        Call the destructor once you're ready with agl_shell_ext interface.
+        This would reset the state and would make any requests made
+        on the agl_shell interface be terminated. The client would need 
+        to bind again the agl_shell_ext and issue a 'doas_shell_client'
+        request.
+      </description>
+    </request>
+
+    <request name="doas_shell_client">
+      <description summary="Informs the compositor it wants to bind to the
+      agl_shell interface">
+        Prior to binding to agl_shell interface, this request would inform
+        the compositor that it wants to gain access the agl_shell interface.
+        The client is expected to wait for 'doas_shell_client_done' event and 
+        check for a successful status before going further with binding to
+        the agl_shell interface.
+      </description>
+    </request>
+
+    <event name="doas_done">
+      <description summary="event sent as a reply to doas_shell_client">
+        The client should check the status event to verify that the
+        compositor was able to handle the request.
+      </description>
+      <arg name="status" type="uint" enum="doas_shell_client_status"/>
+    </event>
   </interface>
 </protocol>
-- 
2.35.1