impl transactional video API methods

Bug: 311265260
Test: CTS coverage
Change-Id: Ib2196ff4bfda7a5a779d939350a00661b121ce9c
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index 40b75a2..f3a2b8a 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -30,6 +30,13 @@
 }
 
 flag{
+  name: "transactional_video_state"
+  namespace: "telecom"
+  description: "when set, clients using transactional implementations will be able to set & get the video state"
+  bug: "311265260"
+}
+
+flag{
   name: "business_call_composer"
   namespace: "telecom"
   description: "Enables enriched calling features (e.g. Business name will show for a call)"
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 624399b..66f9690 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,8 @@
 import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
 import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -652,6 +654,36 @@
     private boolean mIsVideoCallingSupportedByPhoneAccount = false;
 
     /**
+     * Indicates whether this individual calls video state can be changed as opposed to be gated
+     * by the {@link PhoneAccount}.
+     *
+     * {@code True} if the call is Transactional && has the CallAttributes.SUPPORTS_VIDEO_CALLING
+     * capability {@code false} otherwise.
+     */
+    private boolean mTransactionalCallSupportsVideoCalling = false;
+
+    public void setTransactionalCallSupportsVideoCalling(CallAttributes callAttributes) {
+        if (!mIsTransactionalCall) {
+            Log.i(this, "setTransactionalCallSupportsVideoCalling: call is not transactional");
+            return;
+        }
+        if (callAttributes == null) {
+            Log.i(this, "setTransactionalCallSupportsVideoCalling: callAttributes is null");
+            return;
+        }
+        if ((callAttributes.getCallCapabilities() & CallAttributes.SUPPORTS_VIDEO_CALLING)
+                == CallAttributes.SUPPORTS_VIDEO_CALLING) {
+            mTransactionalCallSupportsVideoCalling = true;
+        } else {
+            mTransactionalCallSupportsVideoCalling = false;
+        }
+    }
+
+    public boolean isTransactionalCallSupportsVideoCalling() {
+        return mTransactionalCallSupportsVideoCalling;
+    }
+
+    /**
      * Indicates whether or not this call can be pulled if it is an external call. If true, respect
      * the Connection Capability set by the ConnectionService. If false, override the capability
      * set and always remove the ability to pull this external call.
@@ -4023,6 +4055,15 @@
             videoState = VideoProfile.STATE_AUDIO_ONLY;
         }
 
+        // Transactional calls have the ability to change video calling capabilities on a per-call
+        // basis as opposed to ConnectionService calls which are only based on the PhoneAccount.
+        if (mFlags.transactionalVideoState()
+                && mIsTransactionalCall && !mTransactionalCallSupportsVideoCalling) {
+            Log.i(this, "setVideoState: The transactional does NOT support video calling."
+                    + " defaulted to audio (video not supported)");
+            videoState = VideoProfile.STATE_AUDIO_ONLY;
+        }
+
         // Track Video State history during the duration of the call.
         // Only update the history when the call is active or disconnected. This ensures we do
         // not include the video state history when:
@@ -4045,6 +4086,12 @@
             }
         }
 
+        if (mFlags.transactionalVideoState()
+                && mIsTransactionalCall && mTransactionalService != null) {
+            int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
+            mTransactionalService.onVideoStateChanged(this, transactionalVS);
+        }
+
         if (VideoProfile.isVideo(videoState)) {
             mAnalytics.setCallIsVideo(true);
         }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 4bda96a..d4d395a 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -239,6 +239,9 @@
                                                 callEventCallback, mCallsManager, call);
 
                         call.setTransactionServiceWrapper(serviceWrapper);
+                        if (mFeatureFlags.transactionalVideoState()) {
+                            call.setTransactionalCallSupportsVideoCalling(callAttributes);
+                        }
                         ICallControl clientCallControl = serviceWrapper.getICallControl();
 
                         if (clientCallControl == null) {
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 938ee58..b9096fa 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -43,10 +43,10 @@
 import com.android.server.telecom.voip.HoldCallTransaction;
 import com.android.server.telecom.voip.EndCallTransaction;
 import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
-import com.android.server.telecom.voip.ParallelTransaction;
 import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
 import com.android.server.telecom.voip.SerialTransaction;
 import com.android.server.telecom.voip.SetMuteStateTransaction;
+import com.android.server.telecom.voip.RequestVideoStateTransaction;
 import com.android.server.telecom.voip.TransactionManager;
 import com.android.server.telecom.voip.VoipCallTransaction;
 import com.android.server.telecom.voip.VoipCallTransactionResult;
@@ -71,6 +71,7 @@
     public static final String ANSWER = "Answer";
     public static final String DISCONNECT = "Disconnect";
     public static final String START_STREAMING = "StartStreaming";
+    public static final String REQUEST_VIDEO_STATE = "RequestVideoState";
 
     // CallEventCallback : Telecom --> Client (ex. voip app)
     public static final String ON_SET_ACTIVE = "onSetActive";
@@ -248,6 +249,17 @@
             }
         }
 
+        @Override
+        public void requestVideoState(int videoState, String callId, ResultReceiver callback)
+                throws RemoteException {
+            try {
+                Log.startSession("TSW.rVS");
+                createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState);
+            } finally {
+                Log.endSession();
+            }
+        }
+
         private void createTransactions(String callId, ResultReceiver callback, String action,
                 Object... objects) {
             Log.d(TAG, "createTransactions: callId=" + callId);
@@ -274,6 +286,11 @@
                         addTransactionsToManager(mStreamingController.getStartStreamingTransaction(mCallsManager,
                                 TransactionalServiceWrapper.this, call, mLock), callback);
                         break;
+                    case REQUEST_VIDEO_STATE:
+                        addTransactionsToManager(
+                                new RequestVideoStateTransaction(mCallsManager, call,
+                                        (int) objects[0]), callback);
+                        break;
                 }
             } else {
                 Bundle exceptionBundle = new Bundle();
@@ -562,6 +579,15 @@
         }
     }
 
+    public void onVideoStateChanged(Call call, int videoState) {
+        if (call != null) {
+            try {
+                mICallEventCallback.onVideoStateChanged(call.getId(), videoState);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
     public void removeCallFromWrappers(Call call) {
         if (call != null) {
             try {
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
new file mode 100644
index 0000000..64596b1
--- /dev/null
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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 com.android.server.telecom.voip;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.Call;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class RequestVideoStateTransaction extends VoipCallTransaction {
+
+    private static final String TAG = RequestVideoStateTransaction.class.getSimpleName();
+    private final Call mCall;
+    private final int mVideoProfileState;
+
+    public RequestVideoStateTransaction(CallsManager callsManager, Call call,
+            int transactionalVideoState) {
+        super(callsManager.getLock());
+        mCall = call;
+        mVideoProfileState = TransactionalVideoStateToVideoProfileState(transactionalVideoState);
+    }
+
+    @Override
+    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        Log.d(TAG, "processTransaction");
+        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+        if (isRequestingVideoTransmission(mVideoProfileState) &&
+                !mCall.isVideoCallingSupportedByPhoneAccount()) {
+            future.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_FAILED,
+                    "Video calling is not supported by the target account"));
+        } else if (isRequestingVideoTransmission(mVideoProfileState) &&
+                !mCall.isTransactionalCallSupportsVideoCalling()) {
+            future.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_FAILED,
+                    "Video calling is not supported according to the callAttributes"));
+        } else {
+            mCall.setVideoState(mVideoProfileState);
+            future.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_SUCCEED,
+                    "The Video State was changed successfully"));
+        }
+        return future;
+    }
+
+    private boolean isRequestingVideoTransmission(int targetVideoState) {
+        return targetVideoState != VideoProfile.STATE_AUDIO_ONLY;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
new file mode 100644
index 0000000..615e4bc
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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 com.android.server.telecom.voip;
+
+import android.telecom.CallAttributes;
+import android.telecom.Log;
+import android.telecom.VideoProfile;
+
+/**
+ * This remapping class is needed because {@link VideoProfile} has more fine grain levels of video
+ * states as apposed to Transactional video states (defined in  {@link CallAttributes.CallType}.
+ * To be more specific, there are 3 video states (rx, tx, and bi-directional).
+ * {@link CallAttributes.CallType} only has 2 states (audio and video).
+ *
+ * The reason why Transactional calls have fewer states is due to the fact that the framework is
+ * only used by VoIP apps and Telecom only cares to know if the call is audio or video.
+ *
+ * Calls that are backed by a {@link android.telecom.ConnectionService} have the ability to be
+ * managed calls (non-VoIP) and Dialer needs more fine grain video states to update the UI. Thus,
+ * {@link VideoProfile} is used for {@link android.telecom.ConnectionService} backed calls.
+ */
+public class VideoStateTranslation {
+    private static final String TAG = VideoStateTranslation.class.getSimpleName();
+
+    /**
+     * Client --> Telecom
+     * This should be used when the client application is signaling they are changing the video
+     * state.
+     */
+    public static int TransactionalVideoStateToVideoProfileState(int transactionalVideo) {
+        if (transactionalVideo == CallAttributes.AUDIO_CALL) {
+            Log.i(TAG, "%s --> VideoProfile.STATE_AUDIO_ONLY",
+                    TransactionalVideoState_toString(transactionalVideo));
+            return VideoProfile.STATE_AUDIO_ONLY;
+        } else {
+            Log.i(TAG, "%s --> VideoProfile.STATE_BIDIRECTIONAL",
+                    TransactionalVideoState_toString(transactionalVideo));
+            return VideoProfile.STATE_BIDIRECTIONAL;
+        }
+    }
+
+    /**
+     * Telecom --> Client
+     * This should be used when Telecom is informing the client of a video state change.
+     */
+    public static int VideoProfileStateToTransactionalVideoState(int videoProfileState) {
+        if (videoProfileState == VideoProfile.STATE_AUDIO_ONLY) {
+            Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
+                    VideoProfileState_toString(videoProfileState));
+            return CallAttributes.AUDIO_CALL;
+        } else {
+            Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
+                    VideoProfileState_toString(videoProfileState));
+            return CallAttributes.VIDEO_CALL;
+        }
+    }
+
+    private static String TransactionalVideoState_toString(int transactionalVideoState) {
+        if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
+            return "CallAttributes.AUDIO_CALL";
+        } else {
+            return "CallAttributes.VIDEO_CALL";
+        }
+    }
+
+    private static String VideoProfileState_toString(int videoProfileState) {
+        switch (videoProfileState) {
+            case VideoProfile.STATE_BIDIRECTIONAL -> {
+                return "VideoProfile.STATE_BIDIRECTIONAL";
+            }
+            case VideoProfile.STATE_RX_ENABLED -> {
+                return "VideoProfile.STATE_RX_ENABLED";
+            }
+            case VideoProfile.STATE_TX_ENABLED -> {
+                return "VideoProfile.STATE_TX_ENABLED";
+            }
+        }
+        return "VideoProfile.STATE_AUDIO_ONLY";
+    }
+}