Integrate call composer into the call flow

* Link uploadCallComposerPicture with the actual http upload code
* Perform download of an incoming picture once call filtering completes
* Store uploaded image into the call log once a call has been made
* Add shell commands for CTS testing of the above

Bug: 177613111
Test: atest CallComposerTest

Merged-In: I403e0b4b004b3a22ec0b1dae40f1cc88fee494fc
Change-Id: If20fd2c535b1fc0c79ded7e91bfa10e262e4630e
diff --git a/src/com/android/phone/LocalConnectionImpl.java b/src/com/android/phone/LocalConnectionImpl.java
new file mode 100644
index 0000000..c2630ef
--- /dev/null
+++ b/src/com/android/phone/LocalConnectionImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.phone;
+
+import android.content.Context;
+import android.telephony.TelephonyLocalConnection;
+
+import com.android.phone.callcomposer.CallComposerPictureManager;
+
+import java.util.UUID;
+
+public class LocalConnectionImpl implements TelephonyLocalConnection.ConnectionImpl {
+    private Context mContext;
+
+    public LocalConnectionImpl(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public String getCallComposerServerUrlForHandle(int subscriptionId, UUID uuid) {
+        return CallComposerPictureManager.getInstance(mContext, subscriptionId)
+                .getServerUrlForImageId(uuid);
+    }
+}
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 0aaccea..23f6d7e 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -49,6 +49,7 @@
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyLocalConnection;
 import android.telephony.TelephonyManager;
 import android.telephony.data.ApnSetting;
 import android.util.LocalLog;
@@ -330,6 +331,9 @@
 
         ContentResolver resolver = getContentResolver();
 
+        // Initialize the shim from frameworks/opt/telephony into packages/services/Telephony.
+        TelephonyLocalConnection.setInstance(new LocalConnectionImpl(this));
+
         // Cache the "voice capable" flag.
         // This flag currently comes from a resource (which is
         // overrideable on a per-product basis):
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 59aed4b..2c83ff9 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -185,6 +185,9 @@
 import com.android.internal.telephony.util.VoicemailNotificationSettingsUtil;
 import com.android.internal.util.FunctionalUtils;
 import com.android.internal.util.HexDump;
+import com.android.phone.callcomposer.CallComposerPictureManager;
+import com.android.phone.callcomposer.CallComposerPictureTransfer;
+import com.android.phone.callcomposer.ImageData;
 import com.android.phone.settings.PickSmsSubscriptionActivity;
 import com.android.phone.vvm.PhoneAccountHandleConverter;
 import com.android.phone.vvm.RemoteVvmTaskManager;
@@ -209,7 +212,6 @@
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
-import java.util.UUID;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
@@ -7007,13 +7009,28 @@
                 return;
             }
 
-            // TODO: pass along the bytes read to the carrier somehow
+            if (!readUntilEnd) {
+                loge("Did not finish reading entire image; aborting");
+                return;
+            }
 
-            ParcelUuid result = new ParcelUuid(UUID.randomUUID());
-            // TODO: cache this uuid that's been associated with the picture
-            Bundle outputResult = new Bundle();
-            outputResult.putParcelable(TelephonyManager.KEY_CALL_COMPOSER_PICTURE_HANDLE, result);
-            callback.send(-1, outputResult);
+            ImageData imageData = new ImageData(output.toByteArray(), contentType, null);
+            CallComposerPictureManager.getInstance(mApp, subscriptionId).handleUploadToServer(
+                    new CallComposerPictureTransfer.Factory() {},
+                    imageData,
+                    (result) -> {
+                        if (result.first != null) {
+                            ParcelUuid parcelUuid = new ParcelUuid(result.first);
+                            Bundle outputResult = new Bundle();
+                            outputResult.putParcelable(
+                                    TelephonyManager.KEY_CALL_COMPOSER_PICTURE_HANDLE, parcelUuid);
+                            callback.send(TelephonyManager.CallComposerException.SUCCESS,
+                                    outputResult);
+                        } else {
+                            callback.send(result.second, null);
+                        }
+                    }
+            );
         });
     }
 
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index 5088424..5edd51c 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -21,7 +21,9 @@
 import static com.android.internal.telephony.d2d.Communicator.MESSAGE_DEVICE_BATTERY_STATE;
 import static com.android.internal.telephony.d2d.Communicator.MESSAGE_DEVICE_NETWORK_COVERAGE;
 
+import android.Manifest;
 import android.content.Context;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.PersistableBundle;
 import android.os.Process;
@@ -41,6 +43,7 @@
 import com.android.internal.telephony.emergency.EmergencyNumberTracker;
 import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.modules.utils.BasicShellCommandHandler;
+import com.android.phone.callcomposer.CallComposerPictureManager;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -48,6 +51,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.TreeSet;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 
 /**
  * Takes actions based on the adb commands given by "adb shell cmd phone ...". Be careful, no
@@ -62,6 +67,7 @@
     private static final boolean VDBG = true;
     private static final int DEFAULT_PHONE_ID = 0;
 
+    private static final String CALL_COMPOSER_SUBCOMMAND = "callcomposer";
     private static final String IMS_SUBCOMMAND = "ims";
     private static final String NUMBER_VERIFICATION_SUBCOMMAND = "numverify";
     private static final String EMERGENCY_NUMBER_TEST_MODE = "emergency-number-test-mode";
@@ -69,14 +75,16 @@
     private static final String RESTART_MODEM = "restart-modem";
     private static final String CARRIER_CONFIG_SUBCOMMAND = "cc";
     private static final String DATA_TEST_MODE = "data";
-    private static final String DATA_ENABLE = "enable";
-    private static final String DATA_DISABLE = "disable";
+    private static final String ENABLE = "enable";
+    private static final String DISABLE = "disable";
+    private static final String QUERY = "query";
+
+    private static final String CALL_COMPOSER_TEST_MODE = "test_mode";
+    private static final String CALL_COMPOSER_SIMULATE_CALL = "simulate-outgoing-call";
 
     private static final String IMS_SET_IMS_SERVICE = "set-ims-service";
     private static final String IMS_GET_IMS_SERVICE = "get-ims-service";
     private static final String IMS_CLEAR_SERVICE_OVERRIDE = "clear-ims-service-override";
-    private static final String IMS_ENABLE = "enable";
-    private static final String IMS_DISABLE = "disable";
     // Used to disable or enable processing of conference event package data from the network.
     // This is handy for testing scenarios where CEP data does not exist on a network which does
     // support CEP data.
@@ -192,6 +200,8 @@
                 return handleSingleRegistrationConfigCommand();
             case RESTART_MODEM:
                 return handleRestartModemCommand();
+            case CALL_COMPOSER_SUBCOMMAND:
+                return handleCallComposerCommand();
             default: {
                 return handleDefaultCommands(cmd);
             }
@@ -413,10 +423,10 @@
             case IMS_CLEAR_SERVICE_OVERRIDE: {
                 return handleImsClearCarrierServiceCommand();
             }
-            case IMS_ENABLE: {
+            case ENABLE: {
                 return handleEnableIms();
             }
-            case IMS_DISABLE: {
+            case DISABLE: {
                 return handleDisableIms();
             }
             case IMS_CEP: {
@@ -435,7 +445,7 @@
             return 0;
         }
         switch (arg) {
-            case DATA_ENABLE: {
+            case ENABLE: {
                 try {
                     mInterface.enableDataConnectivity();
                 } catch (RemoteException ex) {
@@ -445,7 +455,7 @@
                 }
                 break;
             }
-            case DATA_DISABLE: {
+            case DISABLE: {
                 try {
                     mInterface.disableDataConnectivity();
                 } catch (RemoteException ex) {
@@ -1664,4 +1674,62 @@
         getOutPrintWriter().println(result);
         return 0;
     }
+
+    private void onHelpCallComposer() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("Call composer commands");
+        pw.println("  callcomposer test-mode enable|disable|query");
+        pw.println("    Enables or disables test mode for call composer. In test mode, picture");
+        pw.println("    upload/download from carrier servers is disabled, and operations are");
+        pw.println("    performed using emulated local files instead.");
+        pw.println("  callcomposer simulate-outgoing-call [subId] [UUID]");
+        pw.println("    Simulates an outgoing call being placed with the picture ID as");
+        pw.println("    the provided UUID. This triggers storage to the call log.");
+    }
+
+    private int handleCallComposerCommand() {
+        String arg = getNextArg();
+        if (arg == null) {
+            onHelpCallComposer();
+            return 0;
+        }
+
+        mContext.enforceCallingPermission(Manifest.permission.MODIFY_PHONE_STATE,
+                "MODIFY_PHONE_STATE required for call composer shell cmds");
+        switch (arg) {
+            case CALL_COMPOSER_TEST_MODE: {
+                String enabledStr = getNextArg();
+                if (ENABLE.equals(enabledStr)) {
+                    CallComposerPictureManager.sTestMode = true;
+                } else if (DISABLE.equals(enabledStr)) {
+                    CallComposerPictureManager.sTestMode = false;
+                } else if (QUERY.equals(enabledStr)) {
+                    getOutPrintWriter().println(CallComposerPictureManager.sTestMode);
+                } else {
+                    onHelpCallComposer();
+                    return 1;
+                }
+                break;
+            }
+            case CALL_COMPOSER_SIMULATE_CALL: {
+                int subscriptionId = Integer.valueOf(getNextArg());
+                String uuidString = getNextArg();
+                UUID uuid = UUID.fromString(uuidString);
+                CompletableFuture<Uri> storageUriFuture = new CompletableFuture<>();
+                Binder.withCleanCallingIdentity(() -> {
+                    CallComposerPictureManager.getInstance(mContext, subscriptionId)
+                            .storeUploadedPictureToCallLog(uuid, storageUriFuture::complete);
+                });
+                try {
+                    Uri uri = storageUriFuture.get();
+                    getOutPrintWriter().println(String.valueOf(uri));
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+                break;
+            }
+        }
+
+        return 0;
+    }
 }
diff --git a/src/com/android/phone/callcomposer/CallComposerPictureManager.java b/src/com/android/phone/callcomposer/CallComposerPictureManager.java
index 81f088a..b27a27c 100644
--- a/src/com/android/phone/callcomposer/CallComposerPictureManager.java
+++ b/src/com/android/phone/callcomposer/CallComposerPictureManager.java
@@ -79,8 +79,8 @@
 
     // disabled provisionally until the auth stack is fully operational
     @VisibleForTesting
-    public static boolean sHttpOperationsEnabled = false;
-    private static final String FAKE_SERVER_URL = "https://example.com/FAKE.png";
+    public static boolean sTestMode = false;
+    public static final String FAKE_SERVER_URL = "https://example.com/FAKE.png";
 
     public interface CallLogProxy {
         default void storeCallComposerPictureAsUser(Context context,
@@ -111,7 +111,7 @@
 
     public void handleUploadToServer(CallComposerPictureTransfer.Factory transferFactory,
             ImageData imageData, Consumer<Pair<UUID, Integer>> callback) {
-        if (!sHttpOperationsEnabled) {
+        if (sTestMode) {
             UUID id = UUID.randomUUID();
             mCachedImages.put(id, imageData);
             mCachedServerUrls.put(id, FAKE_SERVER_URL);
@@ -171,7 +171,7 @@
 
     public void handleDownloadFromServer(CallComposerPictureTransfer.Factory transferFactory,
             String remoteUrl, Consumer<Pair<Uri, Integer>> callback) {
-        if (!sHttpOperationsEnabled) {
+        if (sTestMode) {
             ImageData imageData = new ImageData(getPlaceholderPictureAsBytes(), "image/png", null);
             UUID id = UUID.randomUUID();
             mCachedImages.put(id, imageData);
diff --git a/src/com/android/services/telephony/PstnIncomingCallNotifier.java b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
index ee4baae..235cbce 100644
--- a/src/com/android/services/telephony/PstnIncomingCallNotifier.java
+++ b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
@@ -41,6 +41,7 @@
 import com.android.internal.telephony.imsphone.ImsPhoneConnection;
 import com.android.phone.NumberVerificationManager;
 import com.android.phone.PhoneUtils;
+import com.android.phone.callcomposer.CallComposerPictureManager;
 import com.android.telephony.Rlog;
 
 import java.util.List;
@@ -297,6 +298,11 @@
             if (imsCall != null) {
                 ImsCallProfile imsCallProfile = imsCall.getCallProfile();
                 if (imsCallProfile != null) {
+                    if (CallComposerPictureManager.sTestMode) {
+                        imsCallProfile.setCallExtra(ImsCallProfile.EXTRA_PICTURE_URL,
+                                CallComposerPictureManager.FAKE_SERVER_URL);
+                    }
+
                     extras.putInt(TelecomManager.EXTRA_PRIORITY,
                             imsCallProfile.getCallExtraInt(ImsCallProfile.EXTRA_PRIORITY));
                     extras.putString(TelecomManager.EXTRA_CALL_SUBJECT,
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 5f51efe..971f41b 100755
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -76,6 +76,8 @@
 import com.android.phone.PhoneGlobals;
 import com.android.phone.PhoneUtils;
 import com.android.phone.R;
+import com.android.phone.callcomposer.CallComposerPictureManager;
+import com.android.phone.callcomposer.CallComposerPictureTransfer;
 import com.android.telephony.Rlog;
 
 import java.util.ArrayList;
@@ -1131,13 +1133,33 @@
     @Override
     public void onCallFilteringCompleted(boolean isBlocked, boolean isInContacts) {
         if (isImsConnection()) {
-            if (!isBlocked && isInContacts) {
+            ImsPhone imsPhone = (ImsPhone) getPhone().getImsPhone();
+            if (imsPhone != null
+                    && imsPhone.getCallComposerStatus() == TelephonyManager.CALL_COMPOSER_STATUS_ON
+                    && !isBlocked && isInContacts) {
                 ImsPhoneConnection originalConnection = (ImsPhoneConnection) mOriginalConnection;
                 ImsCallProfile profile = originalConnection.getImsCall().getCallProfile();
                 if (profile != null
                         && !TextUtils.isEmpty(
-                                profile.getCallExtra(ImsCallProfile.EXTRA_PICTURE_URL))) {
-                    // TODO: start off the picture download
+                        profile.getCallExtra(ImsCallProfile.EXTRA_PICTURE_URL))) {
+                    CallComposerPictureManager manager = CallComposerPictureManager
+                            .getInstance(getPhone().getContext(), getPhone().getSubId());
+                    manager.handleDownloadFromServer(new CallComposerPictureTransfer.Factory() {},
+                            profile.getCallExtra(ImsCallProfile.EXTRA_PICTURE_URL),
+                            (result) -> {
+                                if (result.first != null) {
+                                    Bundle newExtras = new Bundle();
+                                    newExtras.putParcelable(TelecomManager.EXTRA_INCOMING_PICTURE,
+                                            result.first);
+                                    putTelephonyExtras(newExtras);
+                                } else {
+                                    Log.i(this, "Call composer picture download:"
+                                            + " error=" + result.second);
+                                    Bundle newExtras = new Bundle();
+                                    newExtras.putBoolean(TelecomManager.EXTRA_HAS_PICTURE, false);
+                                    putTelephonyExtras(newExtras);
+                                }
+                            });
                 }
             }
         }
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 5b2ed38..459c7c4 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -31,6 +31,7 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
+import android.os.ParcelUuid;
 import android.telecom.Conference;
 import android.telecom.Connection;
 import android.telecom.ConnectionRequest;
@@ -70,6 +71,7 @@
 import com.android.phone.MMIDialogActivity;
 import com.android.phone.PhoneUtils;
 import com.android.phone.R;
+import com.android.phone.callcomposer.CallComposerPictureManager;
 import com.android.phone.settings.SuppServicesUiUtil;
 
 import java.lang.ref.WeakReference;
@@ -1709,6 +1711,12 @@
             return;
         }
 
+        if (extras.containsKey(TelecomManager.EXTRA_OUTGOING_PICTURE)) {
+            ParcelUuid uuid = extras.getParcelable(TelecomManager.EXTRA_OUTGOING_PICTURE);
+            CallComposerPictureManager.getInstance(phone.getContext(), phone.getSubId())
+                    .storeUploadedPictureToCallLog(uuid.getUuid(), (uri) -> { });
+        }
+
         com.android.internal.telephony.Connection originalConnection = null;
         try {
             if (phone != null) {