1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.carrierdefaultapp; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Activity; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.os.Bundle; 25 import android.telephony.SubscriptionManager; 26 import android.telephony.TelephonyManager; 27 import android.text.TextUtils; 28 import android.util.Log; 29 import android.view.KeyEvent; 30 import android.webkit.CookieManager; 31 import android.webkit.WebView; 32 import android.webkit.WebViewClient; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.phone.slice.SlicePurchaseController; 36 37 import java.net.URL; 38 import java.util.Base64; 39 40 /** 41 * Activity that launches when the user clicks on the performance boost notification. 42 * This will open a {@link WebView} for the carrier website to allow the user to complete the 43 * premium capability purchase. 44 * The carrier website can get the requested premium capability using the JavaScript interface 45 * method {@code DataBoostWebServiceFlow.getRequestedCapability()}. 46 * If the purchase is successful, the carrier website shall notify the slice purchase application 47 * using the JavaScript interface method 48 * {@code DataBoostWebServiceFlow.notifyPurchaseSuccessful()}. 49 * If the purchase was not successful, the carrier website shall notify the slice purchase 50 * application using the JavaScript interface method 51 * {@code DataBoostWebServiceFlow.notifyPurchaseFailed(code, reason)}, where {@code code} is the 52 * {@link SlicePurchaseController.FailureCode} indicating the reason for failure and {@code reason} 53 * is the human-readable reason for failure if the failure code is 54 * {@link SlicePurchaseController#FAILURE_CODE_UNKNOWN}. 55 * If either of these notification methods are not called, the purchase cannot be completed 56 * successfully and the purchase request will eventually time out. 57 */ 58 public class SlicePurchaseActivity extends Activity { 59 private static final String TAG = "SlicePurchaseActivity"; 60 61 private static final int CONTENTS_TYPE_UNSPECIFIED = 0; 62 private static final int CONTENTS_TYPE_JSON = 1; 63 private static final int CONTENTS_TYPE_XML = 2; 64 65 @NonNull private WebView mWebView; 66 @NonNull private Context mApplicationContext; 67 @NonNull private Intent mIntent; 68 @NonNull private URL mUrl; 69 @TelephonyManager.PremiumCapability protected int mCapability; 70 @Nullable private String mUserData; 71 private int mContentsType; 72 private boolean mIsUserTriggeredFinish; 73 74 @Override onCreate(Bundle savedInstanceState)75 protected void onCreate(Bundle savedInstanceState) { 76 super.onCreate(savedInstanceState); 77 mIntent = getIntent(); 78 int subId = mIntent.getIntExtra(SlicePurchaseController.EXTRA_SUB_ID, 79 SubscriptionManager.INVALID_SUBSCRIPTION_ID); 80 mCapability = mIntent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY, 81 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID); 82 String url = mIntent.getStringExtra(SlicePurchaseController.EXTRA_PURCHASE_URL); 83 mUserData = mIntent.getStringExtra(SlicePurchaseController.EXTRA_USER_DATA); 84 mApplicationContext = getApplicationContext(); 85 mIsUserTriggeredFinish = true; 86 logd("onCreate: subId=" + subId + ", capability=" 87 + TelephonyManager.convertPremiumCapabilityToString(mCapability) + ", url=" + url); 88 89 // Cancel performance boost notification 90 SlicePurchaseBroadcastReceiver.cancelNotification(mApplicationContext, mCapability); 91 92 // Verify purchase URL is valid 93 String contentsType = mIntent.getStringExtra(SlicePurchaseController.EXTRA_CONTENTS_TYPE); 94 mContentsType = CONTENTS_TYPE_UNSPECIFIED; 95 if (!TextUtils.isEmpty(contentsType)) { 96 if (contentsType.equals("json")) { 97 mContentsType = CONTENTS_TYPE_JSON; 98 } else if (contentsType.equals("xml")) { 99 mContentsType = CONTENTS_TYPE_XML; 100 } 101 } 102 mUrl = SlicePurchaseBroadcastReceiver.getPurchaseUrl(url, mUserData, 103 mContentsType == CONTENTS_TYPE_UNSPECIFIED); 104 if (mUrl == null) { 105 String error = "Unable to create a purchase URL."; 106 loge(error); 107 Intent data = new Intent(); 108 data.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE, 109 SlicePurchaseController.FAILURE_CODE_CARRIER_URL_UNAVAILABLE); 110 data.putExtra(SlicePurchaseController.EXTRA_FAILURE_REASON, error); 111 SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext, 112 mIntent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data); 113 finishAndRemoveTask(); 114 return; 115 } 116 117 // Verify user data exists if contents type is specified 118 if (mContentsType != CONTENTS_TYPE_UNSPECIFIED && TextUtils.isEmpty(mUserData)) { 119 String error = "Contents type was specified but user data does not exist."; 120 loge(error); 121 Intent data = new Intent(); 122 data.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE, 123 SlicePurchaseController.FAILURE_CODE_NO_USER_DATA); 124 data.putExtra(SlicePurchaseController.EXTRA_FAILURE_REASON, error); 125 SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext, 126 mIntent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data); 127 finishAndRemoveTask(); 128 return; 129 } 130 131 // Verify intent is valid 132 if (!SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)) { 133 loge("Not starting SlicePurchaseActivity with an invalid Intent: " + mIntent); 134 SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse( 135 mIntent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED); 136 finishAndRemoveTask(); 137 return; 138 } 139 140 // Verify sub ID is valid 141 if (subId != SubscriptionManager.getDefaultSubscriptionId()) { 142 loge("Unable to start the slice purchase application on the non-default data " 143 + "subscription: " + subId); 144 SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse( 145 mIntent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION); 146 finishAndRemoveTask(); 147 return; 148 } 149 150 // Clear any cookies that might be persisted from previous sessions before loading WebView 151 CookieManager.getInstance().removeAllCookies(value -> setupWebView()); 152 } 153 onPurchaseSuccessful()154 protected void onPurchaseSuccessful() { 155 logd("onPurchaseSuccessful: Carrier website indicated successfully purchased premium " 156 + "capability " + TelephonyManager.convertPremiumCapabilityToString(mCapability)); 157 SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse( 158 mIntent, SlicePurchaseController.EXTRA_INTENT_SUCCESS); 159 finishAndRemoveTask(); 160 } 161 onPurchaseFailed(@licePurchaseController.FailureCode int failureCode, @Nullable String failureReason)162 protected void onPurchaseFailed(@SlicePurchaseController.FailureCode int failureCode, 163 @Nullable String failureReason) { 164 logd("onPurchaseFailed: Carrier website indicated purchase failed for premium capability " 165 + TelephonyManager.convertPremiumCapabilityToString(mCapability) + " with code: " 166 + failureCode + " and reason: " + failureReason); 167 Intent data = new Intent(); 168 data.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE, failureCode); 169 data.putExtra(SlicePurchaseController.EXTRA_FAILURE_REASON, failureReason); 170 SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext, 171 mIntent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data); 172 finishAndRemoveTask(); 173 } 174 onDismissFlow()175 protected void onDismissFlow() { 176 logd("onDismissFlow: Dismiss flow called while purchasing premium capability " 177 + TelephonyManager.convertPremiumCapabilityToString(mCapability)); 178 SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse( 179 mIntent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED); 180 finishAndRemoveTask(); 181 } 182 183 @Override onKeyDown(int keyCode, @NonNull KeyEvent event)184 public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { 185 // Pressing back in the WebView will go to the previous page instead of closing 186 // the slice purchase application. 187 if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) { 188 mWebView.goBack(); 189 return true; 190 } 191 return super.onKeyDown(keyCode, event); 192 } 193 194 @Override onDestroy()195 protected void onDestroy() { 196 if (mIsUserTriggeredFinish) { 197 logd("onDestroy: User canceled the purchase by closing the application."); 198 SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse( 199 mIntent, SlicePurchaseController.EXTRA_INTENT_CANCELED); 200 } 201 super.onDestroy(); 202 } 203 204 @Override finishAndRemoveTask()205 public void finishAndRemoveTask() { 206 mIsUserTriggeredFinish = false; 207 super.finishAndRemoveTask(); 208 } 209 setupWebView()210 private void setupWebView() { 211 // Create WebView 212 mWebView = new WebView(this); 213 mWebView.setWebViewClient(new WebViewClient()); 214 215 // Enable JavaScript for the carrier purchase website to send results back to 216 // the slice purchase application. 217 mWebView.getSettings().setJavaScriptEnabled(true); 218 mWebView.addJavascriptInterface( 219 new DataBoostWebServiceFlow(this), "DataBoostWebServiceFlow"); 220 221 // Display WebView 222 setContentView(mWebView); 223 224 // Start the WebView 225 startWebView(mWebView, mUrl.toString(), mContentsType, mUserData); 226 } 227 228 /** 229 * Send the URL to the WebView as either a GET or POST request, based on the contents type: 230 * <ul> 231 * <li> 232 * CONTENTS_TYPE_UNSPECIFIED: 233 * If the user data exists, append it to the purchase URL and load it as a GET request. 234 * If the user data does not exist, load just the purchase URL as a GET request. 235 * </li> 236 * <li> 237 * CONTENTS_TYPE_JSON or CONTENTS_TYPE_XML: 238 * The user data must exist. Send the JSON or XML formatted user data in a POST request. 239 * If the user data is encoded, it must be prefaced by {@code encodedValue=} and will be 240 * encoded in Base64. Decode the user data and send it in the POST request. 241 * </li> 242 * </ul> 243 * @param webView The WebView to start. 244 * @param url The URL to start the WebView with. 245 * @param contentsType The contents type of the userData. 246 * @param userData The user data to send with the GET or POST request, if it exists. 247 */ 248 @VisibleForTesting startWebView(@onNull WebView webView, @NonNull String url, int contentsType, @Nullable String userData)249 public static void startWebView(@NonNull WebView webView, @NonNull String url, int contentsType, 250 @Nullable String userData) { 251 if (contentsType == CONTENTS_TYPE_UNSPECIFIED) { 252 logd("Starting WebView GET with url: " + url); 253 webView.loadUrl(url); 254 } else { 255 byte[] data = userData.getBytes(); 256 String[] split = userData.split("encodedValue="); 257 if (split.length > 1) { 258 logd("Decoding encoded value: " + split[1]); 259 data = Base64.getDecoder().decode(split[1]); 260 } 261 logd("Starting WebView POST with url: " + url + ", contentsType: " + contentsType 262 + ", data: " + new String(data)); 263 webView.postUrl(url, data); 264 } 265 } 266 logd(@onNull String s)267 private static void logd(@NonNull String s) { 268 Log.d(TAG, s); 269 } 270 loge(@onNull String s)271 private static void loge(@NonNull String s) { 272 Log.e(TAG, s); 273 } 274 } 275