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