Make changes to upload code resulting from testing

Make tweaks to the HTTP upload process that were revealed to be
necessary during the live network testing process.

Bug: 182425585
Test: manual
Change-Id: Ib0058bc6de4f8a5e2bb3f4d5b2e84c05d3617fad
diff --git a/src/com/android/phone/callcomposer/CallComposerPictureManager.java b/src/com/android/phone/callcomposer/CallComposerPictureManager.java
index 3c9e27e..818994a 100644
--- a/src/com/android/phone/callcomposer/CallComposerPictureManager.java
+++ b/src/com/android/phone/callcomposer/CallComposerPictureManager.java
@@ -20,6 +20,7 @@
 import android.location.Location;
 import android.net.Uri;
 import android.os.OutcomeReceiver;
+import android.os.PersistableBundle;
 import android.os.UserHandle;
 import android.provider.CallLog;
 import android.telephony.CarrierConfigManager;
@@ -33,14 +34,12 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.phone.callcomposer.CallComposerPictureTransfer.PictureCallback;
 import com.android.phone.R;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 import java.util.HashMap;
-import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
@@ -53,6 +52,7 @@
 public class CallComposerPictureManager {
     private static final String TAG = CallComposerPictureManager.class.getSimpleName();
     private static final SparseArray<CallComposerPictureManager> sInstances = new SparseArray<>();
+    private static final String THREE_GPP_BOOTSTRAPPING = "3GPP-bootstrapping";
 
     public static CallComposerPictureManager getInstance(Context context, int subscriptionId) {
         synchronized (sInstances) {
@@ -104,7 +104,7 @@
 
     private final HashMap<UUID, String> mCachedServerUrls = new HashMap<>();
     private final HashMap<UUID, ImageData> mCachedImages = new HashMap<>();
-    private final Map<String, GbaCredentials> mCachedCredentials = new HashMap<>();
+    private GbaCredentials mCachedCredentials = null;
     private final int mSubscriptionId;
     private final TelephonyManager mTelephonyManager;
     private final Context mContext;
@@ -127,7 +127,8 @@
             return;
         }
 
-        String uploadUrl = mTelephonyManager.getCarrierConfig().getString(
+        PersistableBundle carrierConfig = mTelephonyManager.getCarrierConfig();
+        String uploadUrl = carrierConfig.getString(
                 CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING);
         if (TextUtils.isEmpty(uploadUrl)) {
             Log.e(TAG, "Call composer upload URL not configured in carrier config");
@@ -141,7 +142,7 @@
                 mSubscriptionId, uploadUrl, sExecutorService);
 
         AtomicBoolean hasRetried = new AtomicBoolean(false);
-        transfer.setCallback(new PictureCallback() {
+        transfer.setCallback(new CallComposerPictureTransfer.PictureCallback() {
             @Override
             public void onError(int error) {
                 callback.accept(Pair.create(null, error));
@@ -157,7 +158,7 @@
                 }
                 GbaCredentialsSupplier supplier =
                         (realm, executor) ->
-                                getGbaCredentials(credentialRefresh, realm, executor);
+                                getGbaCredentials(credentialRefresh, carrierConfig, executor);
 
                 sExecutorService.schedule(() -> transfer.uploadPicture(imageData, supplier),
                         backoffMillis, TimeUnit.MILLISECONDS);
@@ -174,7 +175,7 @@
         });
 
         transfer.uploadPicture(imageData,
-                (realm, executor) -> getGbaCredentials(false, realm, executor));
+                (realm, executor) -> getGbaCredentials(false, carrierConfig, executor));
     }
 
     public void handleDownloadFromServer(CallComposerPictureTransfer.Factory transferFactory,
@@ -187,11 +188,12 @@
             return;
         }
 
+        PersistableBundle carrierConfig = mTelephonyManager.getCarrierConfig();
         CallComposerPictureTransfer transfer = transferFactory.create(mContext,
                 mSubscriptionId, remoteUrl, sExecutorService);
 
         AtomicBoolean hasRetried = new AtomicBoolean(false);
-        transfer.setCallback(new PictureCallback() {
+        transfer.setCallback(new CallComposerPictureTransfer.PictureCallback() {
             @Override
             public void onError(int error) {
                 callback.accept(Pair.create(null, error));
@@ -207,7 +209,7 @@
                 }
                 GbaCredentialsSupplier supplier =
                         (realm, executor) ->
-                                getGbaCredentials(credentialRefresh, realm, executor);
+                                getGbaCredentials(credentialRefresh, carrierConfig, executor);
 
                 sExecutorService.schedule(() -> transfer.downloadPicture(supplier),
                         backoffMillis, TimeUnit.MILLISECONDS);
@@ -237,7 +239,8 @@
             }
         });
 
-        transfer.downloadPicture(((realm, executor) -> getGbaCredentials(false, realm, executor)));
+        transfer.downloadPicture(((realm, executor) ->
+                getGbaCredentials(false, carrierConfig, executor)));
     }
 
     public void storeUploadedPictureToCallLog(UUID id, Consumer<Uri> callback) {
@@ -298,32 +301,36 @@
     }
 
     private CompletableFuture<GbaCredentials> getGbaCredentials(
-            boolean forceRefresh, String nafId, Executor executor) {
-        synchronized (mCachedCredentials) {
-            if (!forceRefresh && mCachedCredentials.containsKey(nafId)) {
-                return CompletableFuture.completedFuture(mCachedCredentials.get(nafId));
+            boolean forceRefresh, PersistableBundle config, Executor executor) {
+        synchronized (this) {
+            if (!forceRefresh && mCachedCredentials != null) {
+                return CompletableFuture.completedFuture(mCachedCredentials);
             }
+
             if (forceRefresh) {
-                mCachedCredentials.remove(nafId);
+                mCachedCredentials = null;
             }
         }
 
         UaSecurityProtocolIdentifier securityProtocolIdentifier =
                 new UaSecurityProtocolIdentifier.Builder()
-                        .setOrg(UaSecurityProtocolIdentifier.ORG_3GPP)
-                        .setProtocol(UaSecurityProtocolIdentifier
-                                .UA_SECURITY_PROTOCOL_3GPP_HTTP_DIGEST_AUTHENTICATION)
+                        .setOrg(config.getInt(
+                                CarrierConfigManager.KEY_GBA_UA_SECURITY_ORGANIZATION_INT))
+                        .setProtocol(config.getInt(
+                                CarrierConfigManager.KEY_GBA_UA_SECURITY_PROTOCOL_INT))
+                        .setTlsCipherSuite(config.getInt(
+                                CarrierConfigManager.KEY_GBA_UA_TLS_CIPHER_SUITE_INT))
                         .build();
         CompletableFuture<GbaCredentials> resultFuture = new CompletableFuture<>();
 
-        mTelephonyManager.bootstrapAuthenticationRequest(TelephonyManager.APPTYPE_UNKNOWN,
-                Uri.parse(nafId), securityProtocolIdentifier, forceRefresh, executor,
+        mTelephonyManager.bootstrapAuthenticationRequest(TelephonyManager.APPTYPE_ISIM,
+                getNafUri(config), securityProtocolIdentifier, forceRefresh, executor,
                 new TelephonyManager.BootstrapAuthenticationCallback() {
                     @Override
                     public void onKeysAvailable(byte[] gbaKey, String transactionId) {
                         GbaCredentials creds = new GbaCredentials(transactionId, gbaKey);
-                        synchronized (mCachedCredentials) {
-                            mCachedCredentials.put(nafId, creds);
+                        synchronized (CallComposerPictureManager.this) {
+                            mCachedCredentials = creds;
                         }
                         resultFuture.complete(creds);
                     }
@@ -338,6 +345,30 @@
         return resultFuture;
     }
 
+    private static Uri getNafUri(PersistableBundle carrierConfig) {
+        String uploadUriString = carrierConfig.getString(
+                CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING);
+        Uri uploadUri = Uri.parse(uploadUriString);
+        String nafPrefix;
+        switch (carrierConfig.getInt(CarrierConfigManager.KEY_GBA_MODE_INT)) {
+            case CarrierConfigManager.GBA_U:
+                nafPrefix = THREE_GPP_BOOTSTRAPPING + "-uicc";
+                break;
+            case CarrierConfigManager.GBA_DIGEST:
+                nafPrefix = THREE_GPP_BOOTSTRAPPING + "-digest";
+                break;
+            case CarrierConfigManager.GBA_ME:
+            default:
+                nafPrefix = THREE_GPP_BOOTSTRAPPING;
+        }
+        String newAuthority = nafPrefix + "@" + uploadUri.getAuthority();
+        Uri nafUri = new Uri.Builder().scheme(uploadUri.getScheme())
+                .encodedAuthority(newAuthority)
+                .build();
+        Log.i(TAG, "using NAF uri " + nafUri + " for GBA");
+        return nafUri;
+    }
+
     @VisibleForTesting
     static ScheduledExecutorService getExecutor() {
         return sExecutorService;
diff --git a/src/com/android/phone/callcomposer/CallComposerPictureTransfer.java b/src/com/android/phone/callcomposer/CallComposerPictureTransfer.java
index 1a176dd..e4458cd 100644
--- a/src/com/android/phone/callcomposer/CallComposerPictureTransfer.java
+++ b/src/com/android/phone/callcomposer/CallComposerPictureTransfer.java
@@ -21,6 +21,7 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
+import android.os.Build;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
@@ -49,6 +50,9 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.nio.charset.Charset;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Iterator;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
@@ -140,7 +144,10 @@
                         mExecutorService);
         networkUrlFuture.thenAcceptAsync((result) -> {
             if (result != null) mCallback.onUploadSuccessful(result);
-        }, mExecutorService);
+        }, mExecutorService).exceptionally((ex) -> {
+            logException("Exception uploading image" , ex);
+            return null;
+        });
     }
 
     public void downloadPicture(GbaCredentialsSupplier credentialsSupplier) {
@@ -203,6 +210,9 @@
             }
             if (fromAuth != null) mCallback.onDownloadSuccessful(fromAuth);
             mCallback.onDownloadSuccessful(fromImmediate);
+        }).exceptionally((ex) -> {
+            logException("Exception downloading image" , ex);
+            return null;
         });
     }
 
@@ -223,7 +233,7 @@
         return resultFuture;
     }
 
-    private static HttpURLConnection prepareInitialPost(Network network, String uploadUrl) {
+    private HttpURLConnection prepareInitialPost(Network network, String uploadUrl) {
         try {
             HttpURLConnection connection =
                     (HttpURLConnection) network.openConnection(new URL(uploadUrl));
@@ -231,7 +241,7 @@
             connection.setInstanceFollowRedirects(false);
             connection.setConnectTimeout(HTTP_TIMEOUT_MILLIS);
             connection.setReadTimeout(HTTP_TIMEOUT_MILLIS);
-            connection.setRequestProperty("User-Agent", THREE_GPP_GBA);
+            connection.setRequestProperty("User-Agent", getUserAgent());
             return connection;
         } catch (MalformedURLException e) {
             Log.e(TAG, "Malformed URL: " + uploadUrl);
@@ -242,14 +252,14 @@
         }
     }
 
-    private static HttpURLConnection prepareImageDownloadRequest(Network network, String imageUrl) {
+    private HttpURLConnection prepareImageDownloadRequest(Network network, String imageUrl) {
         try {
             HttpURLConnection connection =
                     (HttpURLConnection) network.openConnection(new URL(imageUrl));
             connection.setRequestMethod("GET");
             connection.setConnectTimeout(HTTP_TIMEOUT_MILLIS);
             connection.setReadTimeout(HTTP_TIMEOUT_MILLIS);
-            connection.setRequestProperty("User-Agent", THREE_GPP_GBA);
+            connection.setRequestProperty("User-Agent", getUserAgent());
             return connection;
         } catch (MalformedURLException e) {
             Log.e(TAG, "Malformed URL: " + imageUrl);
@@ -387,7 +397,7 @@
             public void sendDispositionHeader(OutputStream out) throws IOException {
                 super.sendDispositionHeader(out);
                 if (filename != null) {
-                    String fileNameSuffix = ";filename=\"" + filename + "\"";
+                    String fileNameSuffix = "; filename=\"" + filename + "\"";
                     out.write(fileNameSuffix.getBytes());
                 }
             }
@@ -416,6 +426,11 @@
         HttpURLConnection connection = prepareInitialPost(network, mUrl);
         connection.setDoOutput(true);
         connection.addRequestProperty("Authorization", authHeader);
+        connection.addRequestProperty("Content-Length",
+                String.valueOf(multipartEntity.getContentLength()));
+        connection.addRequestProperty("Content-Type", multipartEntity.getContentType().getValue());
+        connection.addRequestProperty("Accept-Encoding", "*");
+
         try (OutputStream requestBodyOut = connection.getOutputStream()) {
             multipartEntity.writeTo(requestBodyOut);
         } catch (IOException e) {
@@ -425,6 +440,8 @@
 
         try {
             int response = connection.getResponseCode();
+            Log.i(TAG, "Received response code: " + response
+                    + ", message=" + connection.getResponseMessage());
             if (response == 401 || response == 403) {
                 deliverFailure(TelephonyManager.CallComposerException.ERROR_AUTHENTICATION_FAILED);
                 return null;
@@ -493,6 +510,22 @@
         return sb.toString();
     }
 
+    private String getUserAgent() {
+        String carrierName = mContext.getSystemService(TelephonyManager.class)
+                .createForSubscriptionId(mSubscriptionId)
+                .getSimOperatorName();
+        String buildId = Build.ID;
+        String buildDate = DateTimeFormatter.ofPattern("yyyy-MM-dd")
+                .withZone(ZoneId.systemDefault())
+                .format(Instant.ofEpochMilli(Build.TIME));
+        String buildVersion = Build.VERSION.RELEASE_OR_CODENAME;
+        String deviceName = Build.DEVICE;
+        return String.format("%s %s %s %s %s %s %s",
+                carrierName, buildId, buildDate, "Android", buildVersion,
+                deviceName, THREE_GPP_GBA);
+
+    }
+
     private static void logException(String message, Throwable e) {
         StringWriter log = new StringWriter();
         log.append(message);
diff --git a/src/com/android/phone/callcomposer/DigestAuthUtils.java b/src/com/android/phone/callcomposer/DigestAuthUtils.java
index 52a278b..2f081f7 100644
--- a/src/com/android/phone/callcomposer/DigestAuthUtils.java
+++ b/src/com/android/phone/callcomposer/DigestAuthUtils.java
@@ -56,9 +56,13 @@
         if (!TextUtils.isEmpty(parsedHeader.getAlgorithm())
                 && !MD5_ALGORITHM.equals(parsedHeader.getAlgorithm().toLowerCase())) {
             Log.e(TAG, "This client only supports MD5 auth");
+            return "";
         }
-
-        Log.i(TAG, "nonce=" + parsedHeader.getNonce());
+        if (!TextUtils.isEmpty(parsedHeader.getQop())
+                && !AUTH_QOP.equals(parsedHeader.getQop().toLowerCase())) {
+            Log.e(TAG, "This client only supports the auth qop");
+            return "";
+        }
 
         String clientNonce = makeClientNonce();
 
@@ -71,7 +75,9 @@
             replyHeader.setScheme(parsedHeader.getScheme());
             replyHeader.setUsername(credentials.getTransactionId());
             replyHeader.setURI(new WorkaroundURI(uri));
+            replyHeader.setRealm(parsedHeader.getRealm());
             replyHeader.setQop(AUTH_QOP);
+            replyHeader.setNonce(parsedHeader.getNonce());
             replyHeader.setCNonce(clientNonce);
             replyHeader.setNonceCount(1);
             replyHeader.setResponse(response);
@@ -83,7 +89,7 @@
             return null;
         }
 
-        return replyHeader.encode();
+        return replyHeader.encodeBody();
     }
 
     public static String computeResponse(String serverNonce, String clientNonce, String qop,
diff --git a/tests/src/com/android/phone/callcomposer/PictureManagerTest.java b/tests/src/com/android/phone/callcomposer/PictureManagerTest.java
index b52b297..f1ce3b8 100644
--- a/tests/src/com/android/phone/callcomposer/PictureManagerTest.java
+++ b/tests/src/com/android/phone/callcomposer/PictureManagerTest.java
@@ -34,6 +34,7 @@
 import android.provider.CallLog;
 import android.telephony.CarrierConfigManager;
 import android.telephony.TelephonyManager;
+import android.telephony.gba.TlsParams;
 import android.telephony.gba.UaSecurityProtocolIdentifier;
 
 import org.junit.After;
@@ -78,6 +79,14 @@
         PersistableBundle b = new PersistableBundle();
         b.putString(CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING,
                 FAKE_URL_BASE);
+        b.putInt(CarrierConfigManager.KEY_GBA_MODE_INT,
+                CarrierConfigManager.GBA_ME);
+        b.putInt(CarrierConfigManager.KEY_GBA_UA_SECURITY_ORGANIZATION_INT,
+                UaSecurityProtocolIdentifier.ORG_3GPP);
+        b.putInt(CarrierConfigManager.KEY_GBA_UA_SECURITY_PROTOCOL_INT,
+                UaSecurityProtocolIdentifier.UA_SECURITY_PROTOCOL_3GPP_TLS_DEFAULT);
+        b.putInt(CarrierConfigManager.KEY_GBA_UA_TLS_CIPHER_SUITE_INT,
+                TlsParams.TLS_RSA_WITH_AES_128_CBC_SHA);
         when(telephonyManager.getCarrierConfig()).thenReturn(b);
     }
 
@@ -263,7 +272,7 @@
 
     public void testGbaCredLookup(GbaCredentialsSupplier supplier, boolean forceExpected)
             throws Exception {
-        String fakeRealm = "3gpp-bootstraping@naf1.example.com";
+        String fakeNafId = "https://3GPP-bootstrapping@www.example.com";
         byte[] fakeKey = new byte[] {1, 2, 3, 4, 5};
         String fakeTxId = "89sdfjggf";
 
@@ -271,8 +280,9 @@
                 ArgumentCaptor.forClass(TelephonyManager.BootstrapAuthenticationCallback.class);
 
         CompletableFuture<GbaCredentials> credsFuture =
-                supplier.getCredentials(fakeRealm, CallComposerPictureManager.getExecutor());
-        verify(telephonyManager).bootstrapAuthenticationRequest(anyInt(), eq(Uri.parse(fakeRealm)),
+                supplier.getCredentials(fakeNafId, CallComposerPictureManager.getExecutor());
+        verify(telephonyManager).bootstrapAuthenticationRequest(anyInt(),
+                eq(Uri.parse(fakeNafId)),
                 nullable(UaSecurityProtocolIdentifier.class), eq(forceExpected),
                 nullable(Executor.class),
                 authCallbackCaptor.capture());
@@ -285,9 +295,9 @@
         // Do it again and see if we make another request, then make sure that matches up with what
         // we expected.
         CompletableFuture<GbaCredentials> credsFuture1 =
-                supplier.getCredentials(fakeRealm, CallComposerPictureManager.getExecutor());
+                supplier.getCredentials(fakeNafId, CallComposerPictureManager.getExecutor());
         verify(telephonyManager, times(forceExpected ? 2 : 1))
-                .bootstrapAuthenticationRequest(anyInt(), eq(Uri.parse(fakeRealm)),
+                .bootstrapAuthenticationRequest(anyInt(), eq(Uri.parse(fakeNafId)),
                         nullable(UaSecurityProtocolIdentifier.class),
                         eq(forceExpected),
                         nullable(Executor.class),