SlicePurchaseActivity handle user data based on contents type

If contents type is not present, we need to append user data to the url
and send it as a GET request. If contents type is xml or json, we need
to send the user data as a POST request. If user data is encoded, we
need to decode it before sending it in the POST request. If the contents
type is specified but user data does not exist, return an error.

Test: atest CarrierDefaultAppUnitTests
Test: manual test userdata is properly appended or posted
Bug: 282905562
Change-Id: I59e1e28e7e1bd583307da55187886b8bb0798006
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
index b100980..fcc4ec1 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
@@ -31,9 +31,11 @@
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.phone.slice.SlicePurchaseController;
 
 import java.net.URL;
+import java.util.Base64;
 
 /**
  * Activity that launches when the user clicks on the performance boost notification.
@@ -56,11 +58,17 @@
 public class SlicePurchaseActivity extends Activity {
     private static final String TAG = "SlicePurchaseActivity";
 
+    private static final int CONTENTS_TYPE_UNSPECIFIED = 0;
+    private static final int CONTENTS_TYPE_JSON = 1;
+    private static final int CONTENTS_TYPE_XML = 2;
+
     @NonNull private WebView mWebView;
     @NonNull private Context mApplicationContext;
     @NonNull private Intent mIntent;
     @NonNull private URL mUrl;
     @TelephonyManager.PremiumCapability protected int mCapability;
+    @Nullable private String mUserData;
+    private int mContentsType;
     private boolean mIsUserTriggeredFinish;
 
     @Override
@@ -72,6 +80,7 @@
         mCapability = mIntent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
         String url = mIntent.getStringExtra(SlicePurchaseController.EXTRA_PURCHASE_URL);
+        mUserData = mIntent.getStringExtra(SlicePurchaseController.EXTRA_USER_DATA);
         mApplicationContext = getApplicationContext();
         mIsUserTriggeredFinish = true;
         logd("onCreate: subId=" + subId + ", capability="
@@ -81,7 +90,17 @@
         SlicePurchaseBroadcastReceiver.cancelNotification(mApplicationContext, mCapability);
 
         // Verify purchase URL is valid
-        mUrl = SlicePurchaseBroadcastReceiver.getPurchaseUrl(url);
+        String contentsType = mIntent.getStringExtra(SlicePurchaseController.EXTRA_CONTENTS_TYPE);
+        mContentsType = CONTENTS_TYPE_UNSPECIFIED;
+        if (!TextUtils.isEmpty(contentsType)) {
+            if (contentsType.equals("json")) {
+                mContentsType = CONTENTS_TYPE_JSON;
+            } else if (contentsType.equals("xml")) {
+                mContentsType = CONTENTS_TYPE_XML;
+            }
+        }
+        mUrl = SlicePurchaseBroadcastReceiver.getPurchaseUrl(url, mUserData,
+                mContentsType == CONTENTS_TYPE_UNSPECIFIED);
         if (mUrl == null) {
             String error = "Unable to create a purchase URL.";
             loge(error);
@@ -95,6 +114,20 @@
             return;
         }
 
+        // Verify user data exists if contents type is specified
+        if (mContentsType != CONTENTS_TYPE_UNSPECIFIED && TextUtils.isEmpty(mUserData)) {
+            String error = "Contents type was specified but user data does not exist.";
+            loge(error);
+            Intent data = new Intent();
+            data.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE,
+                    SlicePurchaseController.FAILURE_CODE_NO_USER_DATA);
+            data.putExtra(SlicePurchaseController.EXTRA_FAILURE_REASON, error);
+            SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext,
+                    mIntent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data);
+            finishAndRemoveTask();
+            return;
+        }
+
         // Verify intent is valid
         if (!SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)) {
             loge("Not starting SlicePurchaseActivity with an invalid Intent: " + mIntent);
@@ -115,9 +148,7 @@
         }
 
         // Clear any cookies that might be persisted from previous sessions before loading WebView
-        CookieManager.getInstance().removeAllCookies(value -> {
-            setupWebView();
-        });
+        CookieManager.getInstance().removeAllCookies(value -> setupWebView());
     }
 
     protected void onPurchaseSuccessful() {
@@ -190,14 +221,46 @@
         // Display WebView
         setContentView(mWebView);
 
-        // Load the URL
-        String userData = mIntent.getStringExtra(SlicePurchaseController.EXTRA_USER_DATA);
-        if (TextUtils.isEmpty(userData)) {
-            logd("Starting WebView with url: " + mUrl.toString());
-            mWebView.loadUrl(mUrl.toString());
+        // Start the WebView
+        startWebView(mWebView, mUrl.toString(), mContentsType, mUserData);
+    }
+
+    /**
+     * Send the URL to the WebView as either a GET or POST request, based on the contents type:
+     * <ul>
+     *     <li>
+     *         CONTENTS_TYPE_UNSPECIFIED:
+     *         If the user data exists, append it to the purchase URL and load it as a GET request.
+     *         If the user data does not exist, load just the purchase URL as a GET request.
+     *     </li>
+     *     <li>
+     *         CONTENTS_TYPE_JSON or CONTENTS_TYPE_XML:
+     *         The user data must exist. Send the JSON or XML formatted user data in a POST request.
+     *         If the user data is encoded, it must be prefaced by {@code encodedValue=} and will be
+     *         encoded in Base64. Decode the user data and send it in the POST request.
+     *     </li>
+     * </ul>
+     * @param webView The WebView to start.
+     * @param url The URL to start the WebView with.
+     * @param contentsType The contents type of the userData.
+     * @param userData The user data to send with the GET or POST request, if it exists.
+     */
+    @VisibleForTesting
+    public static void startWebView(@NonNull WebView webView, @NonNull String url, int contentsType,
+            @Nullable String userData) {
+        if (contentsType == CONTENTS_TYPE_UNSPECIFIED) {
+            logd("Starting WebView GET with url: " + url);
+            webView.loadUrl(url);
         } else {
-            logd("Starting WebView with url: " + mUrl.toString() + ", userData=" + userData);
-            mWebView.postUrl(mUrl.toString(), userData.getBytes());
+            byte[] data = userData.getBytes();
+            String[] split = userData.split("encodedValue=");
+            if (split.length > 1) {
+                logd("Decoding encoded value: " + split[1]);
+                data = Base64.getDecoder().decode(split[1]);
+            }
+            logd("Starting WebView POST with url: " + url + ", contentsType: " + contentsType
+                    + ", data: " + new String(data));
+            webView.postUrl(url, data);
         }
     }
 
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
index 23b9766..9b33704 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
@@ -173,7 +173,9 @@
         }
 
         String purchaseUrl = intent.getStringExtra(SlicePurchaseController.EXTRA_PURCHASE_URL);
-        if (getPurchaseUrl(purchaseUrl) == null) {
+        String userData = intent.getStringExtra(SlicePurchaseController.EXTRA_USER_DATA);
+        String contentsType = intent.getStringExtra(SlicePurchaseController.EXTRA_CONTENTS_TYPE);
+        if (getPurchaseUrl(purchaseUrl, userData, TextUtils.isEmpty(contentsType)) == null) {
             loge("isIntentValid: invalid purchase URL: " + purchaseUrl);
             return false;
         }
@@ -195,12 +197,39 @@
     }
 
     /**
+     * Get the {@link URL} from the given purchase URL String and user data, if it is valid.
+     *
+     * @param purchaseUrl The purchase URL String to use to create the URL.
+     * @param userData The user data parameter from the entitlement server.
+     * @param shouldAppendUserData If this is {@code true} and the {@code userData} exists,
+     *        the {@code userData} should be appended to the {@code purchaseUrl} to create the URL.
+     *        If this is false, only the {@code purchaseUrl} should be used and the {@code userData}
+     *        will be sent as data to the POST request instead.
+     * @return The URL from the given purchase URL and user data or {@code null} if it is invalid.
+     */
+    @Nullable public static URL getPurchaseUrl(@Nullable String purchaseUrl,
+            @Nullable String userData, boolean shouldAppendUserData) {
+        if (purchaseUrl == null) {
+            return null;
+        }
+        // Only append user data if it exists, otherwise just return the purchase URL
+        if (!shouldAppendUserData || TextUtils.isEmpty(userData)) {
+            return getPurchaseUrl(purchaseUrl);
+        }
+        URL url = getPurchaseUrl(purchaseUrl + "?" + userData);
+        if (url == null) {
+            url = getPurchaseUrl(purchaseUrl);
+        }
+        return url;
+    }
+
+    /**
      * Get the {@link URL} from the given purchase URL String, if it is valid.
      *
      * @param purchaseUrl The purchase URL String to use to create the URL.
      * @return The purchase URL from the given String or {@code null} if it is invalid.
      */
-    @Nullable public static URL getPurchaseUrl(@Nullable String purchaseUrl) {
+    @Nullable private static URL getPurchaseUrl(@Nullable String purchaseUrl) {
         if (!URLUtil.isValidUrl(purchaseUrl)) {
             return null;
         }
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
index cc103fa..1ec180b 100644
--- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.app.NotificationManager;
@@ -33,6 +34,7 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.test.ActivityUnitTestCase;
+import android.webkit.WebView;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -46,6 +48,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Base64;
+
 @RunWith(AndroidJUnit4.class)
 public class SlicePurchaseActivityTest extends ActivityUnitTestCase<SlicePurchaseActivity> {
     private static final String CARRIER = "Some Carrier";
@@ -59,6 +63,7 @@
     @Mock CarrierConfigManager mCarrierConfigManager;
     @Mock NotificationManager mNotificationManager;
     @Mock PersistableBundle mPersistableBundle;
+    @Mock WebView mWebView;
 
     private SlicePurchaseActivity mSlicePurchaseActivity;
     private Context mContext;
@@ -153,4 +158,23 @@
         mSlicePurchaseActivity.onDismissFlow();
         verify(mRequestFailedIntent).send();
     }
+
+    @Test
+    public void testStartWebView() {
+        // unspecified contents type
+        SlicePurchaseActivity.startWebView(mWebView, URL, 0 /* CONTENTS_TYPE_UNSPECIFIED */, null);
+        verify(mWebView).loadUrl(eq(URL));
+
+        // specified contents type with user data
+        String userData = "userData";
+        byte[] userDataBytes = userData.getBytes();
+        SlicePurchaseActivity.startWebView(mWebView, URL, 1 /* CONTENTS_TYPE_JSON */, userData);
+        verify(mWebView).postUrl(eq(URL), eq(userDataBytes));
+
+        // specified contents type with encoded user data
+        byte[] encodedUserData = Base64.getEncoder().encode(userDataBytes);
+        userData = "encodedValue=" + new String(encodedUserData);
+        SlicePurchaseActivity.startWebView(mWebView, URL, 1 /* CONTENTS_TYPE_JSON */, userData);
+        verify(mWebView, times(2)).postUrl(eq(URL), eq(userDataBytes));
+    }
 }
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
index 952789c..61847b5 100644
--- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
@@ -160,14 +160,35 @@
                 "file:///android_asset/slice_store_test.html"
         };
 
+        // test invalid URLs
         for (String url : invalidUrls) {
-            URL purchaseUrl = SlicePurchaseBroadcastReceiver.getPurchaseUrl(url);
+            URL purchaseUrl = SlicePurchaseBroadcastReceiver.getPurchaseUrl(url, null, false);
             assertNull(purchaseUrl);
         }
 
+        // test asset URL
         assertEquals(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE,
                 SlicePurchaseBroadcastReceiver.getPurchaseUrl(
-                        SlicePurchaseController.SLICE_PURCHASE_TEST_FILE).toString());
+                        SlicePurchaseController.SLICE_PURCHASE_TEST_FILE, null, false).toString());
+
+        // test normal URL
+        String validUrl = "http://www.google.com";
+        assertEquals(validUrl,
+                SlicePurchaseBroadcastReceiver.getPurchaseUrl(validUrl, null, false).toString());
+
+        // test normal URL with user data but no append
+        String userData = "encryptedUserData=data";
+        assertEquals(validUrl,
+                SlicePurchaseBroadcastReceiver.getPurchaseUrl(validUrl, userData, false)
+                        .toString());
+
+        // test normal URL with user data and append
+        assertEquals(validUrl + "?" + userData,
+                SlicePurchaseBroadcastReceiver.getPurchaseUrl(validUrl, userData, true).toString());
+
+        // test normal URL without user data and append
+        assertEquals(validUrl,
+                SlicePurchaseBroadcastReceiver.getPurchaseUrl(validUrl, null, true).toString());
     }
 
     @Test