TV: Add sample app tools for triggering client commands (#32365)

* Add sample app tools for triggering test cases

* Fix locking when obtaining passcode from content app

* Fix messages test case

* fix ci

* Restyle TV: Add sample app tools for triggering client commands (#32366)

* Restyled by whitespace

* Restyled by google-java-format

---------

Co-authored-by: Restyled.io <commits@restyled.io>

* fix ci, address comments

* fix ci

* address comments

* address comments

---------

Co-authored-by: restyled-io[bot] <32688539+restyled-io[bot]@users.noreply.github.com>
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/fragments/TerminalFragment.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/fragments/TerminalFragment.java
index 660025e..30faf14 100644
--- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/fragments/TerminalFragment.java
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/fragments/TerminalFragment.java
@@ -24,6 +24,8 @@
   private static String TERMINAL_INSTRUCTIONS =
       "add <vid> [<pid>]    Add app with given vendor ID [1, 2, 9050]. Usage: add 9050\r\n"
           + "remove <endpoint>    Remove app at given endpoint [6, 7, etc]. Usage: remove 6\r\n"
+          + "appobserver <appendpoint> <clientnodeindex> <data> <hint>    Send app observer command to client node of the given app endpoint. Usage: appobserver 4 0 data hint\r\n"
+          + "printclients <appendpoint>  Print list of client nodes for the given app endpoint. Usage: printclients 4\r\n"
           + "setpin <endpoint> <pincode>  Set pincode for app with given endpoint ID. Usage: setpin 6 34567890\r\n"
           + "commission <udc-entry>     Commission given udc-entry using given pincode from corresponding app. Usage:"
           + "commission 0\r\n"
diff --git a/examples/tv-app/android/java/AppImpl.cpp b/examples/tv-app/android/java/AppImpl.cpp
index 3c50d75..1dfae0f 100644
--- a/examples/tv-app/android/java/AppImpl.cpp
+++ b/examples/tv-app/android/java/AppImpl.cpp
@@ -324,6 +324,7 @@
         ChipLogProgress(DeviceLayer, " Looking next=%s ", app->GetApplicationBasicDelegate()->GetCatalogVendorApp()->applicationId);
         if (app->GetApplicationBasicDelegate()->GetCatalogVendorApp()->Matches(vendorApp))
         {
+            // need to think about loading apk here?
             ContentAppPlatform::GetInstance().AddContentApp(app, &contentAppEndpoint, Span<DataVersion>(gDataVersions[i]),
                                                             Span<const EmberAfDeviceType>(gContentAppDeviceType));
             return app;
@@ -422,11 +423,16 @@
             ChipLogProgress(DeviceLayer,
                             "ContentAppFactoryImpl GetAllowedClusterListForStaticEndpoint priviledged vendor accessible clusters "
                             "being returned.");
-            return { chip::app::Clusters::Descriptor::Id,         chip::app::Clusters::OnOff::Id,
-                     chip::app::Clusters::WakeOnLan::Id,          chip::app::Clusters::MediaPlayback::Id,
-                     chip::app::Clusters::LowPower::Id,           chip::app::Clusters::KeypadInput::Id,
-                     chip::app::Clusters::ContentLauncher::Id,    chip::app::Clusters::AudioOutput::Id,
-                     chip::app::Clusters::ApplicationLauncher::Id };
+            return { chip::app::Clusters::Descriptor::Id,
+                     chip::app::Clusters::OnOff::Id,
+                     chip::app::Clusters::WakeOnLan::Id,
+                     chip::app::Clusters::MediaPlayback::Id,
+                     chip::app::Clusters::LowPower::Id,
+                     chip::app::Clusters::KeypadInput::Id,
+                     chip::app::Clusters::ContentLauncher::Id,
+                     chip::app::Clusters::AudioOutput::Id,
+                     chip::app::Clusters::ApplicationLauncher::Id,
+                     chip::app::Clusters::Messages::Id };
         }
         ChipLogProgress(
             DeviceLayer,
@@ -434,7 +440,8 @@
         return { chip::app::Clusters::Descriptor::Id,      chip::app::Clusters::OnOff::Id,
                  chip::app::Clusters::WakeOnLan::Id,       chip::app::Clusters::MediaPlayback::Id,
                  chip::app::Clusters::LowPower::Id,        chip::app::Clusters::KeypadInput::Id,
-                 chip::app::Clusters::ContentLauncher::Id, chip::app::Clusters::AudioOutput::Id };
+                 chip::app::Clusters::ContentLauncher::Id, chip::app::Clusters::AudioOutput::Id,
+                 chip::app::Clusters::Messages::Id };
     }
     return {};
 }
diff --git a/examples/tv-app/android/java/src/com/matter/tv/server/tvapp/MessagesManagerStub.java b/examples/tv-app/android/java/src/com/matter/tv/server/tvapp/MessagesManagerStub.java
index b31f14a..4f115c5 100644
--- a/examples/tv-app/android/java/src/com/matter/tv/server/tvapp/MessagesManagerStub.java
+++ b/examples/tv-app/android/java/src/com/matter/tv/server/tvapp/MessagesManagerStub.java
@@ -31,13 +31,6 @@
   public MessagesManagerStub(int endpoint) {
     this.endpoint = endpoint;
     Log.d(TAG, "MessagesManagerStub: at " + this.endpoint);
-
-    HashMap<Long, String> responseOptions = new HashMap<Long, String>();
-    responseOptions.put(new Long(1), "Yes");
-    responseOptions.put(new Long(2), "No");
-    presentMessages(
-        "31323334353637383930313233343536", 1, 1, 30, 60000, "TestMessage", responseOptions);
-    Log.d(TAG, "MessagesManagerStub: added dummy message");
   }
 
   @Override
diff --git a/examples/tv-app/tv-common/shell/AppTvShellCommands.cpp b/examples/tv-app/tv-common/shell/AppTvShellCommands.cpp
index c66beb9..6b94676 100644
--- a/examples/tv-app/tv-common/shell/AppTvShellCommands.cpp
+++ b/examples/tv-app/tv-common/shell/AppTvShellCommands.cpp
@@ -193,6 +193,11 @@
 #if CHIP_DEVICE_CONFIG_APP_PLATFORM_ENABLED
     streamer_printf(sout, "  add <vid> [<pid>]              Add app with given vendor ID [1, 2, 9050]. Usage: app add 9050\r\n");
     streamer_printf(sout, "  remove <endpoint>              Remove app at given endpoint [6, 7, etc]. Usage: app remove 6\r\n");
+    streamer_printf(sout,
+                    "  appobserver <appendpoint> <clientnodeindex> <data> <hint>    Send app observer command to client node of "
+                    "the given app endpoint. Usage: appobserver 4 0 data hint\r\n");
+    streamer_printf(
+        sout, "  printclients <appendpoint>  Print list of client nodes for the given app endpoint. Usage: printclients 4\r\n");
     streamer_printf(
         sout, "  setpin <endpoint> <pincode>    Set pincode for app with given endpoint ID. Usage: app setpin 6 34567890\r\n");
     streamer_printf(sout,
@@ -277,6 +282,63 @@
 
         return CHIP_NO_ERROR;
     }
+    else if (strcmp(argv[0], "printclients") == 0)
+    {
+        if (argc < 2)
+        {
+            return PrintAllCommands();
+        }
+        char * eptr;
+
+        uint16_t endpoint = (uint16_t) strtol(argv[1], &eptr, 10);
+        ContentApp * app  = ContentAppPlatform::GetInstance().GetContentApp(endpoint);
+        if (app == nullptr)
+        {
+            ChipLogProgress(DeviceLayer, "app not found");
+            return CHIP_ERROR_BAD_REQUEST;
+        }
+        uint8_t count = app->GetClientNodeCount();
+        ChipLogProgress(DeviceLayer, "     node count: %d", count);
+        for (uint8_t i = 0; i < count; i++)
+        {
+            NodeId node = app->GetClientNode(i);
+            ChipLogProgress(DeviceLayer, "     node[%d] " ChipLogFormatX64, i, ChipLogValueX64(node));
+        }
+    }
+#if CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE
+    else if (strcmp(argv[0], "appobserver") == 0)
+    {
+        if (argc < 5)
+        {
+            return PrintAllCommands();
+        }
+        char * eptr;
+
+        uint16_t endpoint = (uint16_t) strtol(argv[1], &eptr, 10);
+        ContentApp * app  = ContentAppPlatform::GetInstance().GetContentApp(endpoint);
+        if (app == nullptr)
+        {
+            ChipLogProgress(DeviceLayer, "app not found");
+            return CHIP_ERROR_BAD_REQUEST;
+        }
+        uint8_t clientNodeIndex = (uint8_t) strtol(argv[2], &eptr, 10);
+        if (clientNodeIndex >= app->GetClientNodeCount())
+        {
+            ChipLogProgress(DeviceLayer, "illegal client node index");
+            return CHIP_ERROR_BAD_REQUEST;
+        }
+        NodeId clientNode = app->GetClientNode(clientNodeIndex);
+
+        char * data         = argv[3];
+        char * encodingHint = argv[4];
+
+        app->SendAppObserverCommand(GetDeviceCommissioner(), clientNode, data, encodingHint);
+
+        ChipLogProgress(DeviceLayer, "sent appobserver command");
+
+        return CHIP_NO_ERROR;
+    }
+#endif // CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE
     else if (strcmp(argv[0], "setpin") == 0)
     {
         if (argc < 3)
diff --git a/examples/tv-app/tv-common/src/AppTv.cpp b/examples/tv-app/tv-common/src/AppTv.cpp
index 939e14b..8fd076b 100644
--- a/examples/tv-app/tv-common/src/AppTv.cpp
+++ b/examples/tv-app/tv-common/src/AppTv.cpp
@@ -549,11 +549,16 @@
             ChipLogProgress(DeviceLayer,
                             "ContentAppFactoryImpl GetAllowedClusterListForStaticEndpoint priviledged vendor accessible clusters "
                             "being returned.");
-            return { chip::app::Clusters::Descriptor::Id,         chip::app::Clusters::OnOff::Id,
-                     chip::app::Clusters::WakeOnLan::Id,          chip::app::Clusters::MediaPlayback::Id,
-                     chip::app::Clusters::LowPower::Id,           chip::app::Clusters::KeypadInput::Id,
-                     chip::app::Clusters::ContentLauncher::Id,    chip::app::Clusters::AudioOutput::Id,
-                     chip::app::Clusters::ApplicationLauncher::Id };
+            return { chip::app::Clusters::Descriptor::Id,
+                     chip::app::Clusters::OnOff::Id,
+                     chip::app::Clusters::WakeOnLan::Id,
+                     chip::app::Clusters::MediaPlayback::Id,
+                     chip::app::Clusters::LowPower::Id,
+                     chip::app::Clusters::KeypadInput::Id,
+                     chip::app::Clusters::ContentLauncher::Id,
+                     chip::app::Clusters::AudioOutput::Id,
+                     chip::app::Clusters::ApplicationLauncher::Id,
+                     chip::app::Clusters::Messages::Id }; // TODO: messages?
         }
         ChipLogProgress(
             DeviceLayer,
@@ -561,7 +566,8 @@
         return { chip::app::Clusters::Descriptor::Id,      chip::app::Clusters::OnOff::Id,
                  chip::app::Clusters::WakeOnLan::Id,       chip::app::Clusters::MediaPlayback::Id,
                  chip::app::Clusters::LowPower::Id,        chip::app::Clusters::KeypadInput::Id,
-                 chip::app::Clusters::ContentLauncher::Id, chip::app::Clusters::AudioOutput::Id };
+                 chip::app::Clusters::ContentLauncher::Id, chip::app::Clusters::AudioOutput::Id,
+                 chip::app::Clusters::Messages::Id };
     }
     return {};
 }
diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CertTestFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CertTestFragment.java
index 340563c..7ee9fa3 100644
--- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CertTestFragment.java
+++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CertTestFragment.java
@@ -310,6 +310,13 @@
         });
 
     runAndWait(
+        "messages_presentMessages",
+        successFailureCallback,
+        () -> {
+          tvCastingApp.messages_presentMessages(kTVApp, "CastingAppTestMessage", callback);
+        });
+
+    runAndWait(
         "mediaPlayback_subscribeToCurrentState",
         successFailureCallback,
         () -> {
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/chip/casting/TvCastingApp.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/chip/casting/TvCastingApp.java
index 2485298..9dd0fa5 100644
--- a/examples/tv-casting-app/android/App/app/src/main/jni/com/chip/casting/TvCastingApp.java
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/chip/casting/TvCastingApp.java
@@ -599,6 +599,9 @@
 
   public native boolean onOff_toggle(ContentApp contentApp, Object responseHandler);
 
+  public native boolean messages_presentMessages(
+      ContentApp contentApp, String messageText, Object responseHandler);
+
   static {
     System.loadLibrary("TvCastingApp");
   }
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/Constants.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/Constants.h
index b194754..735cabf 100644
--- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/Constants.h
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/Constants.h
@@ -37,6 +37,7 @@
     MediaPlayback_Seek,
     MediaPlayback_SkipForward,
     MediaPlayback_SkipBackward,
+    Messages_PresentMessagesRequest,
     ApplicationLauncher_LaunchApp,
     ApplicationLauncher_StopApp,
     ApplicationLauncher_HideApp,
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/TvCastingApp-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/TvCastingApp-JNI.cpp
index 0cbfab8..6dbdfba 100644
--- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/TvCastingApp-JNI.cpp
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/TvCastingApp-JNI.cpp
@@ -865,6 +865,36 @@
     return (err == CHIP_NO_ERROR);
 }
 
+JNI_METHOD(jboolean, messages_1presentMessages)
+(JNIEnv * env, jobject, jobject contentApp, jstring messageText, jobject jResponseHandler)
+{
+    chip::DeviceLayer::StackLock lock;
+
+    ChipLogProgress(AppServer, "JNI_METHOD messages_presentMessages called");
+    const char * nativeMessageText = env->GetStringUTFChars(messageText, 0);
+
+    TargetEndpointInfo endpoint;
+    CHIP_ERROR err = convertJContentAppToTargetEndpointInfo(contentApp, endpoint);
+    VerifyOrExit(err == CHIP_NO_ERROR,
+                 ChipLogError(AppServer, "Conversion from jobject contentApp to TargetEndpointInfo * failed: %" CHIP_ERROR_FORMAT,
+                              err.Format()));
+
+    err = TvCastingAppJNIMgr().getMediaCommandResponseHandler(Messages_PresentMessagesRequest).SetUp(env, jResponseHandler);
+    VerifyOrExit(CHIP_NO_ERROR == err,
+                 ChipLogError(AppServer, "MatterCallbackHandlerJNI.SetUp failed %" CHIP_ERROR_FORMAT, err.Format()));
+
+    err = CastingServer::GetInstance()->Messages_PresentMessagesRequest(&endpoint, nativeMessageText, [](CHIP_ERROR err) {
+        TvCastingAppJNIMgr().getMediaCommandResponseHandler(Messages_PresentMessagesRequest).Handle(err);
+    });
+    VerifyOrExit(CHIP_NO_ERROR == err,
+                 ChipLogError(AppServer, "CastingServer.Messages_PresentMessagesRequest failed %" CHIP_ERROR_FORMAT, err.Format()));
+
+    env->ReleaseStringUTFChars(messageText, nativeMessageText);
+
+exit:
+    return (err == CHIP_NO_ERROR);
+}
+
 JNI_METHOD(jboolean, mediaPlayback_1play)
 (JNIEnv * env, jobject, jobject contentApp, jobject jResponseHandler)
 {
diff --git a/examples/tv-casting-app/tv-casting-common/BUILD.gn b/examples/tv-casting-app/tv-casting-common/BUILD.gn
index ea6607a..72f14fb 100644
--- a/examples/tv-casting-app/tv-casting-common/BUILD.gn
+++ b/examples/tv-casting-app/tv-casting-common/BUILD.gn
@@ -68,6 +68,7 @@
     "include/MediaPlayback.h",
     "include/MediaReadBase.h",
     "include/MediaSubscriptionBase.h",
+    "include/Messages.h",
     "include/OnOff.h",
     "include/PersistenceManager.h",
     "include/TargetEndpointInfo.h",
@@ -83,6 +84,7 @@
     "src/KeypadInput.cpp",
     "src/LevelControl.cpp",
     "src/MediaPlayback.cpp",
+    "src/Messages.cpp",
     "src/OnOff.cpp",
     "src/PersistenceManager.cpp",
     "src/TargetEndpointInfo.cpp",
diff --git a/examples/tv-casting-app/tv-casting-common/include/CastingServer.h b/examples/tv-casting-app/tv-casting-common/include/CastingServer.h
index 349edae..d64f504 100644
--- a/examples/tv-casting-app/tv-casting-common/include/CastingServer.h
+++ b/examples/tv-casting-app/tv-casting-common/include/CastingServer.h
@@ -27,6 +27,7 @@
 #include "KeypadInput.h"
 #include "LevelControl.h"
 #include "MediaPlayback.h"
+#include "Messages.h"
 #include "OnOff.h"
 #include "PersistenceManager.h"
 #include "TargetEndpointInfo.h"
@@ -187,6 +188,12 @@
     CHIP_ERROR OnOff_Toggle(TargetEndpointInfo * endpoint, std::function<void(CHIP_ERROR)> responseCallback);
 
     /**
+     * @brief Messages cluster
+     */
+    CHIP_ERROR Messages_PresentMessagesRequest(TargetEndpointInfo * endpoint, const char * messageText,
+                                               std::function<void(CHIP_ERROR)> responseCallback);
+
+    /**
      * @brief Media Playback cluster
      */
     CHIP_ERROR MediaPlayback_Play(TargetEndpointInfo * endpoint, std::function<void(CHIP_ERROR)> responseCallback);
@@ -511,6 +518,11 @@
     ToggleCommand mToggleCommand;
 
     /**
+     * @brief OnOff cluster
+     */
+    PresentMessagesRequestCommand mPresentMessagesRequestCommand;
+
+    /**
      * @brief Media Playback cluster
      */
     PlayCommand mPlayCommand;
diff --git a/examples/tv-casting-app/tv-casting-common/include/Messages.h b/examples/tv-casting-app/tv-casting-common/include/Messages.h
new file mode 100644
index 0000000..623ede9
--- /dev/null
+++ b/examples/tv-casting-app/tv-casting-common/include/Messages.h
@@ -0,0 +1,36 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include "MediaCommandBase.h"
+#include "MediaSubscriptionBase.h"
+
+#include <controller/CHIPCluster.h>
+#include <functional>
+
+#include <app-common/zap-generated/cluster-objects.h>
+
+// COMMAND CLASSES
+class PresentMessagesRequestCommand : public MediaCommandBase<chip::app::Clusters::Messages::Commands::PresentMessagesRequest::Type,
+                                                              chip::app::DataModel::NullObjectType>
+{
+public:
+    PresentMessagesRequestCommand() : MediaCommandBase(chip::app::Clusters::Messages::Id) {}
+
+    CHIP_ERROR
+    Invoke(const char * messageText, std::function<void(CHIP_ERROR)> responseCallback);
+};
diff --git a/examples/tv-casting-app/tv-casting-common/src/CastingServer.cpp b/examples/tv-casting-app/tv-casting-common/src/CastingServer.cpp
index a3de0a2..4cb7273 100644
--- a/examples/tv-casting-app/tv-casting-common/src/CastingServer.cpp
+++ b/examples/tv-casting-app/tv-casting-common/src/CastingServer.cpp
@@ -870,6 +870,16 @@
 }
 
 /**
+ * @brief Messages cluster
+ */
+CHIP_ERROR CastingServer::Messages_PresentMessagesRequest(TargetEndpointInfo * endpoint, const char * messageText,
+                                                          std::function<void(CHIP_ERROR)> responseCallback)
+{
+    ReturnErrorOnFailure(mPresentMessagesRequestCommand.SetTarget(mActiveTargetVideoPlayerInfo, endpoint->GetEndpointId()));
+    return mPresentMessagesRequestCommand.Invoke(messageText, responseCallback);
+}
+
+/**
  * @brief Media Playback cluster
  */
 CHIP_ERROR CastingServer::MediaPlayback_Play(TargetEndpointInfo * endpoint, std::function<void(CHIP_ERROR)> responseCallback)
diff --git a/examples/tv-casting-app/tv-casting-common/src/Messages.cpp b/examples/tv-casting-app/tv-casting-common/src/Messages.cpp
new file mode 100644
index 0000000..ea6176f
--- /dev/null
+++ b/examples/tv-casting-app/tv-casting-common/src/Messages.cpp
@@ -0,0 +1,38 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include "Messages.h"
+
+using namespace chip;
+using namespace chip::app;
+using namespace chip::app::Clusters;
+using namespace chip::app::Clusters::Messages;
+
+CHIP_ERROR PresentMessagesRequestCommand::Invoke(const char * messageText, std::function<void(CHIP_ERROR)> responseCallback)
+{
+    Messages::Commands::PresentMessagesRequest::Type request;
+    uint8_t buf[16] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5 };
+
+    request.messageID   = ByteSpan(buf, sizeof(buf));
+    request.messageText = CharSpan::fromCharString(messageText);
+    request.priority    = MessagePriorityEnum(static_cast<uint8_t>(0));
+    request.startTime   = DataModel::Nullable<uint32_t>(static_cast<uint32_t>(0));
+    request.duration    = DataModel::Nullable<uint64_t>(static_cast<uint64_t>(60 * 1000));
+
+    return MediaCommandBase::Invoke(request, responseCallback);
+}
diff --git a/src/app/app-platform/ContentApp.cpp b/src/app/app-platform/ContentApp.cpp
index 3bc16e9..9a6b4ad 100644
--- a/src/app/app-platform/ContentApp.cpp
+++ b/src/app/app-platform/ContentApp.cpp
@@ -44,6 +44,8 @@
 #define ZCL_DESCRIPTOR_CLUSTER_REVISION (1u)
 #define ZCL_APPLICATION_BASIC_CLUSTER_REVISION (1u)
 
+inline constexpr EndpointId kCastingVideoPlayerEndpointId = 1;
+
 Status ContentApp::HandleReadAttribute(ClusterId clusterId, AttributeId attributeId, uint8_t * buffer, uint16_t maxReadLength)
 {
     ChipLogProgress(DeviceLayer,
@@ -62,6 +64,113 @@
     return Status::Failure;
 }
 
+void ContentApp::AddClientNode(NodeId subjectNodeId)
+{
+    mClientNodes[mNextClientNodeIndex++] = subjectNodeId;
+    if (mClientNodeCount < kMaxClientNodes)
+    {
+        mClientNodeCount++;
+    }
+    if (mNextClientNodeIndex >= kMaxClientNodes)
+    {
+        // if we exceed the max number, then overwrite the oldest entry
+        mNextClientNodeIndex = 0;
+    }
+}
+
+void ContentApp::SendAppObserverCommand(chip::Controller::DeviceCommissioner * commissioner, NodeId clientNodeId, char * data,
+                                        char * encodingHint)
+{
+    ChipLogProgress(Controller, "Attempting to send AppObserver command");
+    if (mContentAppClientCommandSender.IsBusy())
+    {
+        ChipLogProgress(Controller, "SendAppObserverCommand busy");
+        return;
+    }
+
+    mContentAppClientCommandSender.SendContentAppMessage(commissioner, clientNodeId, kCastingVideoPlayerEndpointId, data,
+                                                         encodingHint);
+
+    ChipLogProgress(Controller, "Completed send of AppObserver command");
+}
+
+CHIP_ERROR ContentAppClientCommandSender::SendContentAppMessage(chip::Controller::DeviceCommissioner * commissioner,
+                                                                chip::NodeId destinationId, chip::EndpointId endPointId,
+                                                                char * data, char * encodingHint)
+{
+    if (mIsBusy)
+    {
+        return CHIP_ERROR_INCORRECT_STATE;
+    }
+    mIsBusy        = true;
+    mDestinationId = destinationId;
+    mEndPointId    = endPointId;
+    mData          = std::string(data);
+    mEncodingHint  = std::string(encodingHint);
+
+    ChipLogProgress(Controller, "Sending command to node 0x" ChipLogFormatX64, ChipLogValueX64(mDestinationId));
+
+    return commissioner->GetConnectedDevice(mDestinationId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback);
+}
+
+void ContentAppClientCommandSender::OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr,
+                                                        const chip::SessionHandle & sessionHandle)
+{
+    ChipLogProgress(Controller, "ContentAppClientCommandSender::OnDeviceConnectedFn");
+    ContentAppClientCommandSender * sender = reinterpret_cast<ContentAppClientCommandSender *>(context);
+    VerifyOrReturn(sender != nullptr, ChipLogError(chipTool, "OnDeviceConnectedFn: context is null"));
+
+    sender->SendMessage(exchangeMgr, sessionHandle);
+}
+
+CHIP_ERROR ContentAppClientCommandSender::SendMessage(chip::Messaging::ExchangeManager & exchangeMgr,
+                                                      const chip::SessionHandle & sessionHandle)
+{
+    ChipLogProgress(Controller, "ContentAppClientCommandSender::SendMessage");
+
+    chip::Controller::ClusterBase cluster(exchangeMgr, sessionHandle, mEndPointId);
+
+    chip::app::Clusters::ContentAppObserver::Commands::ContentAppMessage::Type request;
+    request.data         = Optional<CharSpan>(CharSpan::fromCharString(mData.c_str()));
+    request.encodingHint = CharSpan::fromCharString(mEncodingHint.c_str());
+    CHIP_ERROR err       = cluster.InvokeCommand(request, nullptr, OnCommandResponse, OnCommandFailure);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogDetail(Controller, "ContentAppClientCommandSender SendMessage error err %s", ErrorStr(err));
+    }
+
+    mIsBusy = false;
+    ChipLogProgress(Controller, "ContentAppClientCommandSender: Completed send of AppObserver command");
+
+    return CHIP_NO_ERROR;
+}
+
+void ContentAppClientCommandSender::OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR err)
+{
+    ChipLogProgress(Controller, "ContentAppClientCommandSender::OnDeviceConnectedFn error err %s", ErrorStr(err));
+
+    ContentAppClientCommandSender * sender = reinterpret_cast<ContentAppClientCommandSender *>(context);
+    VerifyOrReturn(sender != nullptr, ChipLogError(chipTool, "OnDeviceConnectionFailureFn: context is null"));
+
+    sender->Cleanup();
+}
+
+void ContentAppClientCommandSender::Cleanup()
+{
+    ChipLogProgress(Controller, "ContentAppClientCommandSender::Cleanup");
+    mIsBusy = false;
+}
+
+void ContentAppClientCommandSender::OnCommandResponse(void * context, const ContentAppMessageResponseDecodableType & response)
+{
+    ChipLogProgress(Controller, "ContentAppClientCommandSender::OnCommandResponse");
+}
+
+void ContentAppClientCommandSender::OnCommandFailure(void * context, CHIP_ERROR error)
+{
+    ChipLogProgress(Controller, "ContentAppClientCommandSender::OnCommandFailure error err %s", ErrorStr(error));
+}
+
 } // namespace AppPlatform
 } // namespace chip
 
diff --git a/src/app/app-platform/ContentApp.h b/src/app/app-platform/ContentApp.h
index d3b7d1f..34b1dd5 100644
--- a/src/app/app-platform/ContentApp.h
+++ b/src/app/app-platform/ContentApp.h
@@ -33,6 +33,7 @@
 #include <app/clusters/media-playback-server/media-playback-delegate.h>
 #include <app/clusters/target-navigator-server/target-navigator-delegate.h>
 #include <app/util/attribute-storage.h>
+#include <controller/CHIPDeviceController.h>
 #include <protocols/interaction_model/StatusCode.h>
 
 namespace chip {
@@ -48,6 +49,46 @@
 using MediaPlaybackDelegate       = app::Clusters::MediaPlayback::Delegate;
 using TargetNavigatorDelegate     = app::Clusters::TargetNavigator::Delegate;
 
+inline constexpr uint8_t kMaxClientNodes = 8;
+
+class ContentAppClientCommandSender
+{
+public:
+    ContentAppClientCommandSender() :
+        mOnDeviceConnectedCallback(OnDeviceConnectedFn, this), mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureFn, this)
+    {}
+
+    bool IsBusy() const { return mIsBusy; }
+    CHIP_ERROR SendContentAppMessage(chip::Controller::DeviceCommissioner * commissioner, chip::NodeId destinationId,
+                                     chip::EndpointId endPointId, char * data, char * encodingHint);
+
+protected:
+    CHIP_ERROR SendMessage(chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle);
+
+    void Cleanup();
+
+private:
+    static void OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr,
+                                    const chip::SessionHandle & sessionHandle);
+    static void OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR error);
+
+    using ContentAppMessageResponseDecodableType =
+        chip::app::Clusters::ContentAppObserver::Commands::ContentAppMessageResponse::DecodableType;
+
+    static void OnCommandResponse(void * context, const ContentAppMessageResponseDecodableType & response);
+    static void OnCommandFailure(void * context, CHIP_ERROR error);
+
+    chip::Callback::Callback<chip::OnDeviceConnected> mOnDeviceConnectedCallback;
+    chip::Callback::Callback<chip::OnDeviceConnectionFailure> mOnDeviceConnectionFailureCallback;
+
+    bool mIsBusy                 = false;
+    chip::NodeId mDestinationId  = 0;
+    chip::EndpointId mEndPointId = 0;
+
+    std::string mData;
+    std::string mEncodingHint;
+};
+
 class DLL_EXPORT ContentApp
 {
 public:
@@ -70,8 +111,21 @@
                                                             uint16_t maxReadLength);
     Protocols::InteractionModel::Status HandleWriteAttribute(ClusterId clusterId, AttributeId attributeId, uint8_t * buffer);
 
+    void AddClientNode(NodeId clientNodeId);
+    uint8_t GetClientNodeCount() const { return mClientNodeCount; }
+    NodeId GetClientNode(uint8_t index) const { return mClientNodes[index]; }
+
+    void SendAppObserverCommand(chip::Controller::DeviceCommissioner * commissioner, NodeId clientNodeId, char * data,
+                                char * encodingHint);
+
 protected:
     EndpointId mEndpointId = 0;
+
+    uint8_t mClientNodeCount     = 0;
+    uint8_t mNextClientNodeIndex = 0;
+    NodeId mClientNodes[kMaxClientNodes];
+
+    ContentAppClientCommandSender mContentAppClientCommandSender;
 };
 
 } // namespace AppPlatform
diff --git a/src/app/app-platform/ContentAppPlatform.cpp b/src/app/app-platform/ContentAppPlatform.cpp
index 09d6c55..3d17a52 100644
--- a/src/app/app-platform/ContentAppPlatform.cpp
+++ b/src/app/app-platform/ContentAppPlatform.cpp
@@ -716,6 +716,7 @@
                 continue;
             }
 
+            bool accessAllowed = false;
             for (const auto & allowedVendor : app->GetApplicationBasicDelegate()->GetAllowedVendorList())
             {
                 if (allowedVendor == targetVendorId)
@@ -732,6 +733,12 @@
                         .fabricIndex = kUndefinedFabricIndex,
                     });
                 }
+                accessAllowed = true;
+            }
+            if (accessAllowed)
+            {
+                // notify content app about this nodeId
+                app->AddClientNode(subjectNodeId);
             }
         }
     }
diff --git a/src/controller/CommissionerDiscoveryController.cpp b/src/controller/CommissionerDiscoveryController.cpp
index 86849a3..ee94bc8 100644
--- a/src/controller/CommissionerDiscoveryController.cpp
+++ b/src/controller/CommissionerDiscoveryController.cpp
@@ -110,8 +110,25 @@
     ChipLogDetail(Controller, "------Via Shell Enter: controller ux ok|cancel");
 }
 
+/// Callback for getting execution into the main chip thread
+void CallbackOk(System::Layer * aSystemLayer, void * aAppState)
+{
+    ChipLogDetail(AppServer, "UX Ok: now on main thread");
+    CommissionerDiscoveryController * cdc = static_cast<CommissionerDiscoveryController *>(aAppState);
+    cdc->InternalOk();
+}
+
 void CommissionerDiscoveryController::Ok()
 {
+    ChipLogDetail(AppServer, "UX Ok: moving to main thread");
+    // need to ensure callback is on main chip thread
+    assertChipStackLockedByCurrentThread();
+    DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds32(0), CallbackOk, this);
+}
+
+void CommissionerDiscoveryController::InternalOk()
+{
+    ChipLogDetail(AppServer, "UX InternalOk");
     if (!mPendingConsent)
     {
         ChipLogError(AppServer, "UX Ok: no current instance");
diff --git a/src/controller/CommissionerDiscoveryController.h b/src/controller/CommissionerDiscoveryController.h
index 42a0bb6..f0dcf6d 100644
--- a/src/controller/CommissionerDiscoveryController.h
+++ b/src/controller/CommissionerDiscoveryController.h
@@ -273,6 +273,7 @@
      * indicated in the UserPrompter's PromptForCommissionOKPermission callback
      */
     void Ok();
+    void InternalOk();
 
     /**
      * This method should be called after the user has declined to give consent for commissioning of the client