Improve invocation cancellation message

For cancellations due to command state change, the messages is changed
to:
Invocation was interrupted due to: Command (requestId=##, commandId=##) has been marked canceled by the cluster

Bug: 151332080
Test: unit tests
Test: verified the captured exception by canceling an invocation from atp.
Change-Id: I06ed46e522169ae6a699a94586acd6cde100f930
diff --git a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
index 4d59b96..96f6de0 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
@@ -375,13 +375,18 @@
                                                 mCommandTask.getRequestId(),
                                                 mCommandTask.getCommandId());
                         if (ClusterCommand.State.CANCELED.equals(status)) {
-                            CLog.w(
-                                    "Cluster marked command %s %s as canceled, stopping invocation",
-                                    mCommandTask.getRequestId(), mCommandTask.getCommandId());
+                            // TODO: retrieve cancel reason from TFC.
+                            String cause =
+                                    String.format(
+                                            "Command (requestId=%s, commandId=%s) has been marked"
+                                                    + " canceled by the cluster",
+                                            mCommandTask.getRequestId(),
+                                            mCommandTask.getCommandId());
+                            CLog.w("Stop invocation due to: %s", cause);
                             Optional.ofNullable(getInvocationContext())
                                     .map(IInvocationContext::getInvocationId)
                                     .map(Ints::tryParse)
-                                    .ifPresent(ClusterCommandScheduler.this::stopInvocation);
+                                    .ifPresent(invocationId -> stopInvocation(invocationId, cause));
                         }
                     }
 
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 711ca17..56a069d 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -1840,15 +1840,16 @@
         return false;
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public synchronized boolean stopInvocation(int invocationId) {
+    public synchronized boolean stopInvocation(int invocationId, String cause) {
         // TODO: make invocationID part of InvocationContext
         for (InvocationThread thread : mInvocationThreadMap.values()) {
             if (thread.mCmd.getCommandTracker().mId == invocationId) {
-                thread.stopInvocation("User requested stopping invocation " + invocationId);
+                if (cause == null) {
+                    cause = "User requested stopping invocation " + invocationId;
+                }
+                thread.stopInvocation(cause);
                 return true;
             }
         }
diff --git a/src/com/android/tradefed/command/ICommandScheduler.java b/src/com/android/tradefed/command/ICommandScheduler.java
index 3169982..de10acb 100644
--- a/src/com/android/tradefed/command/ICommandScheduler.java
+++ b/src/com/android/tradefed/command/ICommandScheduler.java
@@ -243,7 +243,20 @@
      * @return true if the invocation was stopped, false otherwise
      * @throws UnsupportedOperationException if the implementation doesn't support this
      */
-    public boolean stopInvocation(int invocationId) throws UnsupportedOperationException;
+    public default boolean stopInvocation(int invocationId) throws UnsupportedOperationException {
+        return stopInvocation(invocationId, null);
+    }
+
+    /**
+     * Stop a running invocation by specifying it's id.
+     *
+     * @param invocationId the tracking id of the invocation.
+     * @param cause the cause for stopping the invocation.
+     * @return true if the invocation was stopped, false otherwise
+     * @throws UnsupportedOperationException if the implementation doesn't support this
+     */
+    public boolean stopInvocation(int invocationId, String cause)
+            throws UnsupportedOperationException;
 
     /**
      * Return the information on an invocation bu specifying the invocation id.
diff --git a/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java b/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
index 1b0699e..c458040 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
@@ -907,7 +907,7 @@
         }
 
         @Override
-        public boolean stopInvocation(int invocationId) {
+        public boolean stopInvocation(int invocationId, String cause) {
             return (stopInvocationCalled = true);
         }