[Android] Add Status handling for onReport code path (#32082)

diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt
index 3e45d62..e0a95d5 100644
--- a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt
+++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt
@@ -10,6 +10,7 @@
 import chip.devicecontroller.model.DataVersionFilter
 import chip.devicecontroller.model.EventState
 import chip.devicecontroller.model.NodeState
+import chip.devicecontroller.model.Status
 import com.matter.controller.commands.common.CredentialsIssuer
 import java.util.logging.Level
 import java.util.logging.Logger
@@ -34,13 +35,6 @@
       eventPath: ChipEventPath?,
       e: Exception
     ) {
-      if (attributePath != null && attributePath.clusterId.getId() == UNIT_TEST_CLUSTER) {
-        logger.log(
-          Level.INFO,
-          "TODO: skip the error check for unit test cluster that covers most error result"
-        )
-        return
-      }
       logger.log(Level.INFO, "Read receive onError")
       setFailure("read failure")
     }
@@ -72,11 +66,20 @@
       return cluster.equals(expected)
     }
 
+    fun checkUnitTestClusterGeneralStatus(status: Status): Boolean =
+      (status.getStatus() == CLUSTER_ID_TEST_GENERAL_ERROR_STATUS) &&
+        !status.getClusterStatus().isPresent()
+
+    fun checkUnitTestClusterClusterStatus(status: Status): Boolean =
+      (status.getStatus() == CLUSTER_ID_TEST_CLUSTER_ERROR_STATUS) &&
+        status.getClusterStatus().isPresent() &&
+        status.getClusterStatus().get() == CLUSTER_ID_TEST_CLUSTER_ERROR_CLUSTER_STATUS
+
     private fun validateResponse(nodeState: NodeState) {
       val endpointZero =
         requireNotNull(nodeState.getEndpointState(0)) { "Endpoint zero not found." }
 
-      val endpointOne = requireNotNull(nodeState.getEndpointState(0)) { "Endpoint one not found." }
+      val endpointOne = requireNotNull(nodeState.getEndpointState(1)) { "Endpoint one not found." }
 
       val basicCluster =
         requireNotNull(endpointZero.getClusterState(CLUSTER_ID_BASIC)) {
@@ -93,6 +96,11 @@
           "No local config disabled attribute found."
         }
 
+      val unitTestCluster =
+        requireNotNull(endpointOne.getClusterState(UNIT_TEST_CLUSTER)) {
+          "Unit test cluster not found."
+        }
+
       val startUpEvents =
         requireNotNull(basicCluster.getEventState(EVENT_ID_START_UP)) { "No start up event found." }
 
@@ -122,6 +130,22 @@
       require(checkAllAttributesJsonForFixedLabel(clusterAttributes)) {
         "Invalid fixed label cluster attributes Json ${clusterAttributes}"
       }
+
+      require(
+        checkUnitTestClusterGeneralStatus(
+          unitTestCluster.getAttributeStatuses()[CLUSTER_ID_TEST_GENERAL_ERROR_BOOLEAN]!!
+        )
+      ) {
+        "Invalid unit test cluster generalStatus check ${unitTestCluster}"
+      }
+
+      require(
+        checkUnitTestClusterClusterStatus(
+          unitTestCluster.getAttributeStatuses()[CLUSTER_ID_TEST_CLUSTER_ERROR_BOOLEAN]!!
+        )
+      ) {
+        "Invalid unit test cluster clusterStatus check ${unitTestCluster}"
+      }
     }
 
     override fun onReport(nodeState: NodeState) {
@@ -212,10 +236,15 @@
     private const val MATTER_PORT = 5540
     private const val CLUSTER_ID_BASIC = 0x0028L
     private const val FIXED_LABEL_CLUSTER = 0x0040L
-    private const val UNIT_TEST_CLUSTER = 0xfff1fc05
+    private const val UNIT_TEST_CLUSTER = 0xfff1fc05L
     private const val ATTR_ID_LOCAL_CONFIG_DISABLED = 16L
     private const val EVENT_ID_START_UP = 0L
     private const val GLOBAL_ATTRIBUTE_LIST = 65531L
     private const val CLUSTER_ID_BASIC_VERSION = 0L
+    private const val CLUSTER_ID_TEST_GENERAL_ERROR_BOOLEAN = 0x0031L
+    private const val CLUSTER_ID_TEST_CLUSTER_ERROR_BOOLEAN = 0x0032L
+    private const val CLUSTER_ID_TEST_GENERAL_ERROR_STATUS = 0x8d
+    private const val CLUSTER_ID_TEST_CLUSTER_ERROR_STATUS = 1
+    private const val CLUSTER_ID_TEST_CLUSTER_ERROR_CLUSTER_STATUS = 17
   }
 }
diff --git a/src/controller/java/AndroidCallbacks.cpp b/src/controller/java/AndroidCallbacks.cpp
index 88bbf98..9e3e3e9 100644
--- a/src/controller/java/AndroidCallbacks.cpp
+++ b/src/controller/java/AndroidCallbacks.cpp
@@ -55,22 +55,6 @@
     return CHIP_NO_ERROR;
 }
 
-CHIP_ERROR ReportCallback::CreateChipEventPath(JNIEnv * env, const app::ConcreteEventPath & aPath, jobject & outObj)
-{
-    jclass eventPathCls = nullptr;
-    ReturnErrorOnFailure(
-        JniReferences::GetInstance().GetLocalClassRef(env, "chip/devicecontroller/model/ChipEventPath", eventPathCls));
-
-    jmethodID eventPathCtor =
-        env->GetStaticMethodID(eventPathCls, "newInstance", "(IJJ)Lchip/devicecontroller/model/ChipEventPath;");
-    VerifyOrReturnError(eventPathCtor != nullptr, CHIP_JNI_ERROR_METHOD_NOT_FOUND);
-
-    outObj = env->CallStaticObjectMethod(eventPathCls, eventPathCtor, static_cast<jint>(aPath.mEndpointId),
-                                         static_cast<jlong>(aPath.mClusterId), static_cast<jlong>(aPath.mEventId));
-    VerifyOrReturnError(outObj != nullptr, CHIP_JNI_ERROR_NULL_OBJECT);
-    return CHIP_NO_ERROR;
-}
-
 GetConnectedDeviceCallback::GetConnectedDeviceCallback(jobject wrapperCallback, jobject javaCallback) :
     mOnSuccess(OnDeviceConnectedFn, this), mOnFailure(OnDeviceConnectionFailureFn, this)
 {
@@ -270,6 +254,30 @@
     return TlvToJson(readerForJson, json);
 }
 
+static CHIP_ERROR CreateStatus(JNIEnv * env, const app::StatusIB & aStatus, jobject & outObj)
+{
+    jclass statusCls = nullptr;
+    ReturnErrorOnFailure(JniReferences::GetInstance().GetLocalClassRef(env, "chip/devicecontroller/model/Status", statusCls));
+    jmethodID statusCtor = nullptr;
+    if (aStatus.mClusterStatus.HasValue())
+    {
+        statusCtor = env->GetStaticMethodID(statusCls, "newInstance", "(II)Lchip/devicecontroller/model/Status;");
+        VerifyOrReturnError(!env->ExceptionCheck(), CHIP_JNI_ERROR_EXCEPTION_THROWN);
+        VerifyOrReturnError(statusCtor != nullptr, CHIP_JNI_ERROR_METHOD_NOT_FOUND);
+        outObj = env->CallStaticObjectMethod(statusCls, statusCtor, static_cast<jint>(aStatus.mStatus),
+                                             static_cast<jint>(aStatus.mClusterStatus.Value()));
+    }
+    else
+    {
+        statusCtor = env->GetStaticMethodID(statusCls, "newInstance", "(I)Lchip/devicecontroller/model/Status;");
+        VerifyOrReturnError(!env->ExceptionCheck(), CHIP_JNI_ERROR_EXCEPTION_THROWN);
+        VerifyOrReturnError(statusCtor != nullptr, CHIP_JNI_ERROR_METHOD_NOT_FOUND);
+        outObj = env->CallStaticObjectMethod(statusCls, statusCtor, static_cast<jint>(aStatus.mStatus));
+    }
+    VerifyOrReturnError(outObj != nullptr, CHIP_JNI_ERROR_METHOD_NOT_FOUND);
+    return CHIP_NO_ERROR;
+}
+
 void ReportCallback::OnAttributeData(const app::ConcreteDataAttributePath & aPath, TLV::TLVReader * apData,
                                      const app::StatusIB & aStatus)
 {
@@ -277,14 +285,28 @@
     JNIEnv * env   = JniReferences::GetInstance().GetEnvForCurrentThread();
     VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread"));
     JniLocalReferenceScope scope(env);
-
-    jobject attributePathObj = nullptr;
-    err                      = CreateChipAttributePath(env, aPath, attributePathObj);
-    VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to create Java ChipAttributePath: %s", ErrorStr(err)));
-
     VerifyOrReturn(!aPath.IsListItemOperation(), ChipLogError(Controller, "Expect non-list item operation"); aPath.LogPath());
-    VerifyOrReturn(aStatus.IsSuccess(), ChipLogError(Controller, "Receive bad status %s", ErrorStr(aStatus.ToChipError()));
-                   aPath.LogPath());
+
+    jobject nodeState = mNodeStateObj.ObjectRef();
+    if (aStatus.IsFailure())
+    {
+        ChipLogError(Controller, "Receive bad status %s", ErrorStr(aStatus.ToChipError()));
+        jobject statusObj = nullptr;
+        err               = CreateStatus(env, aStatus, statusObj);
+        VerifyOrReturn(err == CHIP_NO_ERROR,
+                       ChipLogError(Controller, "Fail to create status with error %" CHIP_ERROR_FORMAT, err.Format()));
+        // Add Attribute Status to NodeState
+        jmethodID addAttributeStatusMethod = nullptr;
+        err                                = JniReferences::GetInstance().FindMethod(env, nodeState, "addAttributeStatus",
+                                                                                     "(IJJLchip/devicecontroller/model/Status;)V", &addAttributeStatusMethod);
+        VerifyOrReturn(
+            err == CHIP_NO_ERROR,
+            ChipLogError(Controller, "Could not find addAttributeStatus method with error %" CHIP_ERROR_FORMAT, err.Format()));
+        env->CallVoidMethod(nodeState, addAttributeStatusMethod, static_cast<jint>(aPath.mEndpointId),
+                            static_cast<jlong>(aPath.mClusterId), static_cast<jlong>(aPath.mAttributeId), statusObj);
+        VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe());
+        return;
+    }
     VerifyOrReturn(apData != nullptr, ChipLogError(Controller, "Receive empty apData"); aPath.LogPath());
 
     TLV::TLVReader readerForJavaTLV;
@@ -345,7 +367,6 @@
     VerifyOrReturn(attributeStateObj != nullptr, ChipLogError(Controller, "Could not create AttributeState object");
                    aPath.LogPath());
 
-    jobject nodeState = mNodeStateObj.ObjectRef();
     // Add AttributeState to NodeState
     jmethodID addAttributeMethod;
     err = JniReferences::GetInstance().FindMethod(env, nodeState, "addAttribute",
@@ -401,10 +422,28 @@
     CHIP_ERROR err = CHIP_NO_ERROR;
     JNIEnv * env   = JniReferences::GetInstance().GetEnvForCurrentThread();
     VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread"));
-    jobject eventPathObj = nullptr;
-    err                  = CreateChipEventPath(env, aEventHeader.mPath, eventPathObj);
-    VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to create Java ChipEventPath: %s", ErrorStr(err)));
 
+    jobject nodeState = mNodeStateObj.ObjectRef();
+    if (apStatus != nullptr && apStatus->IsFailure())
+    {
+        ChipLogError(Controller, "Receive bad status %s", ErrorStr(apStatus->ToChipError()));
+        jobject statusObj = nullptr;
+        err               = CreateStatus(env, *apStatus, statusObj);
+        VerifyOrReturn(err == CHIP_NO_ERROR,
+                       ChipLogError(Controller, "Fail to create status with error %" CHIP_ERROR_FORMAT, err.Format()));
+        // Add Event Status to NodeState
+        jmethodID addEventStatusMethod;
+        err = JniReferences::GetInstance().FindMethod(env, nodeState, "addEventStatus",
+                                                      "(IJJLchip/devicecontroller/model/Status;)V", &addEventStatusMethod);
+        VerifyOrReturn(
+            err == CHIP_NO_ERROR,
+            ChipLogError(Controller, "Could not find addEventStatus method with error %" CHIP_ERROR_FORMAT, err.Format()));
+        env->CallVoidMethod(nodeState, addEventStatusMethod, static_cast<jint>(aEventHeader.mPath.mEndpointId),
+                            static_cast<jlong>(aEventHeader.mPath.mClusterId), static_cast<jlong>(aEventHeader.mPath.mEventId),
+                            statusObj);
+        VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe());
+        return;
+    }
     VerifyOrReturn(apData != nullptr, ChipLogError(Controller, "Receive empty apData"); aEventHeader.LogPath());
 
     TLV::TLVReader readerForJavaTLV;
@@ -484,7 +523,7 @@
 
     // Add EventState to NodeState
     jmethodID addEventMethod;
-    jobject nodeState = mNodeStateObj.ObjectRef();
+
     err = JniReferences::GetInstance().FindMethod(env, nodeState, "addEvent", "(IJJLchip/devicecontroller/model/EventState;)V",
                                                   &addEventMethod);
     VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find addEvent method with error %s", ErrorStr(err));
diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn
index 0f68fce..0e5beda 100644
--- a/src/controller/java/BUILD.gn
+++ b/src/controller/java/BUILD.gn
@@ -482,6 +482,7 @@
     "src/chip/devicecontroller/model/EventState.java",
     "src/chip/devicecontroller/model/InvokeElement.java",
     "src/chip/devicecontroller/model/NodeState.java",
+    "src/chip/devicecontroller/model/Status.java",
   ]
 
   if (matter_enable_tlv_decoder_api) {
diff --git a/src/controller/java/src/chip/devicecontroller/model/ClusterState.java b/src/controller/java/src/chip/devicecontroller/model/ClusterState.java
index 4c575f1..6ebe40e 100644
--- a/src/controller/java/src/chip/devicecontroller/model/ClusterState.java
+++ b/src/controller/java/src/chip/devicecontroller/model/ClusterState.java
@@ -32,12 +32,19 @@
   private static final String TAG = "ClusterState";
   private Map<Long, AttributeState> attributes;
   private Map<Long, ArrayList<EventState>> events;
+  private Map<Long, Status> attributeStatuses;
+  private Map<Long, ArrayList<Status>> eventStatuses;
   private Optional<Long> dataVersion;
 
-  public ClusterState(
-      Map<Long, AttributeState> attributes, Map<Long, ArrayList<EventState>> events) {
+  protected ClusterState(
+      Map<Long, AttributeState> attributes,
+      Map<Long, ArrayList<EventState>> events,
+      Map<Long, Status> attributeStatuses,
+      Map<Long, ArrayList<Status>> eventStatuses) {
     this.attributes = attributes;
     this.events = events;
+    this.attributeStatuses = attributeStatuses;
+    this.eventStatuses = eventStatuses;
     this.dataVersion = Optional.empty();
   }
 
@@ -45,10 +52,18 @@
     return attributes;
   }
 
+  public Map<Long, Status> getAttributeStatuses() {
+    return attributeStatuses;
+  }
+
   public Map<Long, ArrayList<EventState>> getEventStates() {
     return events;
   }
 
+  public Map<Long, ArrayList<Status>> getEventStatuses() {
+    return eventStatuses;
+  }
+
   public void setDataVersion(long version) {
     dataVersion = Optional.of(version);
   }
@@ -130,6 +145,25 @@
                 builder.append("\n");
               });
         });
+    attributeStatuses.forEach(
+        (attributeId, status) -> {
+          builder.append("Attribute Status ");
+          builder.append(attributeId);
+          builder.append(": ");
+          builder.append(status.toString());
+          builder.append("\n");
+        });
+    eventStatuses.forEach(
+        (eventId, status) -> {
+          status.forEach(
+              (eventState) -> {
+                builder.append("Event Status");
+                builder.append(eventId);
+                builder.append(": ");
+                builder.append(status.toString());
+                builder.append("\n");
+              });
+        });
     return builder.toString();
   }
 }
diff --git a/src/controller/java/src/chip/devicecontroller/model/NodeState.java b/src/controller/java/src/chip/devicecontroller/model/NodeState.java
index decd978..f77097b 100644
--- a/src/controller/java/src/chip/devicecontroller/model/NodeState.java
+++ b/src/controller/java/src/chip/devicecontroller/model/NodeState.java
@@ -55,10 +55,15 @@
 
     ClusterState clusterState = endpointState.getClusterState(clusterId);
     if (clusterState == null) {
-      clusterState = new ClusterState(new HashMap<>(), new HashMap<>());
+      clusterState =
+          new ClusterState(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
       endpointState.getClusterStates().put(clusterId, clusterState);
     }
 
+    if (clusterState.getAttributeStatuses().containsKey(attributeId)) {
+      clusterState.getAttributeStatuses().remove(attributeId);
+    }
+
     // This will overwrite previous attributes.
     clusterState.getAttributeStates().put(attributeId, attributeStateToAdd);
   }
@@ -72,16 +77,70 @@
 
     ClusterState clusterState = endpointState.getClusterState(clusterId);
     if (clusterState == null) {
-      clusterState = new ClusterState(new HashMap<>(), new HashMap<>());
+      clusterState =
+          new ClusterState(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
       endpointState.getClusterStates().put(clusterId, clusterState);
     }
 
     if (!clusterState.getEventStates().containsKey(eventId)) {
       clusterState.getEventStates().put(eventId, new ArrayList<EventState>());
     }
+
+    if (clusterState.getEventStatuses().containsKey(eventId)) {
+      clusterState.getEventStatuses().remove(eventId);
+    }
+
     clusterState.getEventStates().get(eventId).add(eventStateToAdd);
   }
 
+  // Called from native code only, which ignores access modifiers.
+  private void addAttributeStatus(
+      int endpointId, long clusterId, long attributeId, Status statusToAdd) {
+    EndpointState endpointState = getEndpointState(endpointId);
+    if (endpointState == null) {
+      endpointState = new EndpointState(new HashMap<>());
+      getEndpointStates().put(endpointId, endpointState);
+    }
+
+    ClusterState clusterState = endpointState.getClusterState(clusterId);
+    if (clusterState == null) {
+      clusterState =
+          new ClusterState(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
+      endpointState.getClusterStates().put(clusterId, clusterState);
+    }
+
+    if (clusterState.getAttributeStates().containsKey(attributeId)) {
+      clusterState.getAttributeStates().remove(attributeId);
+    }
+
+    clusterState.getAttributeStatuses().put(attributeId, statusToAdd);
+  }
+
+  private void addEventStatus(int endpointId, long clusterId, long eventId, Status statusToAdd) {
+    EndpointState endpointState = getEndpointState(endpointId);
+    if (endpointState == null) {
+      endpointState = new EndpointState(new HashMap<>());
+      getEndpointStates().put(endpointId, endpointState);
+    }
+
+    ClusterState clusterState = endpointState.getClusterState(clusterId);
+    if (clusterState == null) {
+      clusterState =
+          new ClusterState(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
+      endpointState.getClusterStates().put(clusterId, clusterState);
+    }
+
+    if (!clusterState.getEventStatuses().containsKey(eventId)) {
+      clusterState.getEventStatuses().put(eventId, new ArrayList<Status>());
+    }
+
+    if (clusterState.getEventStates().containsKey(eventId)) {
+      clusterState.getEventStates().remove(eventId);
+    }
+
+    clusterState.getEventStatuses().get(eventId).add(statusToAdd);
+  }
+
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
diff --git a/src/controller/java/src/chip/devicecontroller/model/Status.java b/src/controller/java/src/chip/devicecontroller/model/Status.java
new file mode 100644
index 0000000..7391f9a
--- /dev/null
+++ b/src/controller/java/src/chip/devicecontroller/model/Status.java
@@ -0,0 +1,57 @@
+/*
+ *   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.
+ *
+ */
+
+package chip.devicecontroller.model;
+
+import java.util.Locale;
+import java.util.Optional;
+
+public final class Status {
+  private Integer status;
+  private Optional<Integer> clusterStatus;
+
+  private Status(int status, Optional<Integer> clusterStatus) {
+    this.status = status;
+    this.clusterStatus = clusterStatus;
+  }
+
+  // Getters
+  public Integer getStatus() {
+    return status;
+  }
+
+  public Optional<Integer> getClusterStatus() {
+    return clusterStatus;
+  }
+
+  public String toString() {
+    return String.format(
+        Locale.ENGLISH,
+        "status %s, clusterStatus %s",
+        String.valueOf(status),
+        clusterStatus.isPresent() ? String.valueOf(clusterStatus.get()) : "None");
+  }
+
+  public static Status newInstance(int status, int clusterStatus) {
+    return new Status(status, Optional.of(clusterStatus));
+  }
+
+  public static Status newInstance(int status) {
+    return new Status(status, Optional.empty());
+  }
+}