1 /*
2  * Copyright (C) 2021 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.imsserviceentitlement;
18 
19 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
20 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
21 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
22 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__INCOMPATIBLE;
23 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL;
24 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__TIMEOUT;
25 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT;
26 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
27 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__ACTIVATION;
28 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UPDATE;
29 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI;
30 
31 import android.app.Activity;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.os.CountDownTimer;
35 import android.text.TextUtils;
36 import android.util.Log;
37 
38 import androidx.annotation.MainThread;
39 import androidx.annotation.Nullable;
40 import androidx.annotation.StringRes;
41 
42 import com.android.imsserviceentitlement.entitlement.EntitlementResult;
43 import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus;
44 import com.android.imsserviceentitlement.utils.ImsUtils;
45 import com.android.imsserviceentitlement.utils.MetricsLogger;
46 import com.android.imsserviceentitlement.utils.TelephonyUtils;
47 
48 import com.google.common.annotations.VisibleForTesting;
49 
50 import java.time.Duration;
51 
52 /**
53  * The driver for WFC activation workflow: go/vowifi-entitlement-status-analysis.
54  *
55  * <p>One {@link WfcActivationActivity} owns one and only one controller instance.
56  */
57 public class WfcActivationController {
58     private static final String TAG = "IMSSE-WfcActivationController";
59 
60     // Entitlement status update retry
61     private static final int ENTITLEMENT_STATUS_UPDATE_RETRY_MAX = 6;
62     private static final long ENTITLEMENT_STATUS_UPDATE_RETRY_INTERVAL_MS =
63             Duration.ofSeconds(5).toMillis();
64 
65     // Dependencies
66     private final WfcActivationUi mActivationUi;
67     private final TelephonyUtils mTelephonyUtils;
68     private final ImsEntitlementApi mImsEntitlementApi;
69     private final ImsUtils mImsUtils;
70     private final Intent mStartIntent;
71     private final MetricsLogger mMetricsLogger;
72 
73     // States
74     private int mEvaluateTimes = 0;
75     private int mAppResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
76 
77     @MainThread
WfcActivationController( Context context, WfcActivationUi wfcActivationUi, ImsEntitlementApi imsEntitlementApi, Intent intent)78     public WfcActivationController(
79             Context context,
80             WfcActivationUi wfcActivationUi,
81             ImsEntitlementApi imsEntitlementApi,
82             Intent intent) {
83         this.mStartIntent = intent;
84         this.mActivationUi = wfcActivationUi;
85         this.mImsEntitlementApi = imsEntitlementApi;
86         this.mTelephonyUtils = new TelephonyUtils(context, getSubId());
87         this.mImsUtils = ImsUtils.getInstance(context, getSubId());
88         this.mMetricsLogger = new MetricsLogger(mTelephonyUtils);
89     }
90 
91     @VisibleForTesting
WfcActivationController( Context context, WfcActivationUi wfcActivationUi, ImsEntitlementApi imsEntitlementApi, Intent intent, ImsUtils imsUtils, MetricsLogger metricsLogger)92     WfcActivationController(
93             Context context,
94             WfcActivationUi wfcActivationUi,
95             ImsEntitlementApi imsEntitlementApi,
96             Intent intent,
97             ImsUtils imsUtils,
98             MetricsLogger metricsLogger) {
99         this.mStartIntent = intent;
100         this.mActivationUi = wfcActivationUi;
101         this.mImsEntitlementApi = imsEntitlementApi;
102         this.mTelephonyUtils = new TelephonyUtils(context, getSubId());
103         this.mImsUtils = imsUtils;
104         this.mMetricsLogger = metricsLogger;
105     }
106 
107     /** Indicates the controller to start WFC activation or emergency address update flow. */
108     @MainThread
startFlow()109     public void startFlow() {
110         showGeneralWaitingUi();
111         evaluateEntitlementStatus();
112         if (isActivationFlow()) {
113             mMetricsLogger.start(IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__ACTIVATION);
114         } else {
115             mMetricsLogger.start(IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UPDATE);
116         }
117     }
118 
119     /** Evaluates entitlement status for activation or update. */
120     @MainThread
evaluateEntitlementStatus()121     public void evaluateEntitlementStatus() {
122         if (!mTelephonyUtils.isNetworkConnected()) {
123             handleInitialEntitlementStatus(null);
124             return;
125         }
126         EntitlementUtils.entitlementCheck(
127                 mImsEntitlementApi, result -> handleInitialEntitlementStatus(result));
128     }
129 
130     /**
131      * Indicates the controller to re-evaluate WFC entitlement status after activation flow finished
132      * successfully (ie. not canceled) by user.
133      */
134     @MainThread
finishFlow()135     public void finishFlow() {
136         showGeneralWaitingUi();
137         reevaluateEntitlementStatus();
138     }
139 
140     /** Re-evaluate entitlement status after updating. */
141     @MainThread
reevaluateEntitlementStatus()142     public void reevaluateEntitlementStatus() {
143         EntitlementUtils.entitlementCheck(
144                 mImsEntitlementApi, result -> handleReevaluationEntitlementStatus(result));
145     }
146 
147     /** The interface for handling the entitlement check result. */
148     public interface EntitlementResultCallback {
onEntitlementResult(EntitlementResult result)149         void onEntitlementResult(EntitlementResult result);
150     }
151 
152     /** Indicates the controller to finish on-going tasks and get ready to be destroyed. */
153     @MainThread
finish()154     public void finish() {
155         EntitlementUtils.cancelEntitlementCheck();
156 
157         // If no result set, it must be cancelled by user pressing back button.
158         if (mAppResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) {
159             mAppResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
160         }
161         mMetricsLogger.write(IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI, mAppResult);
162     }
163 
164     /**
165      * Returns {@code true} if the app is launched for WFC activation; {@code false} for emergency
166      * address update.
167      */
isActivationFlow()168     private boolean isActivationFlow() {
169         return ActivityConstants.isActivationFlow(mStartIntent);
170     }
171 
getSubId()172     private int getSubId() {
173         return ActivityConstants.getSubId(mStartIntent);
174     }
175 
176     /** Returns UI title string resource ID based on {@link #isActivationFlow()}. */
177     @StringRes
getUiTitle()178     private int getUiTitle() {
179         int intention = ActivityConstants.getLaunchIntention(mStartIntent);
180         if (intention == ActivityConstants.LAUNCH_APP_ACTIVATE) {
181             return R.string.activate_title;
182         }
183         if (intention == ActivityConstants.LAUNCH_APP_SHOW_TC) {
184             return R.string.tos_title;
185         }
186         // LAUNCH_APP_UPDATE or otherwise
187         return R.string.e911_title;
188     }
189 
190     /** Returns general error string resource ID based on {@link #isActivationFlow()}. */
191     @StringRes
getGeneralErrorText()192     private int getGeneralErrorText() {
193         int intention = ActivityConstants.getLaunchIntention(mStartIntent);
194         if (intention == ActivityConstants.LAUNCH_APP_ACTIVATE) {
195             return R.string.wfc_activation_error;
196         } else if (intention == ActivityConstants.LAUNCH_APP_SHOW_TC) {
197             return R.string.show_terms_and_condition_error;
198         }
199         // LAUNCH_APP_UPDATE or otherwise
200         return R.string.address_update_error;
201     }
202 
showErrorUi(@tringRes int errorMessage)203     private void showErrorUi(@StringRes int errorMessage) {
204         mActivationUi.showActivationUi(
205                 getUiTitle(), errorMessage, false, R.string.ok, WfcActivationUi.RESULT_FAILURE, 0);
206     }
207 
showGeneralErrorUi()208     private void showGeneralErrorUi() {
209         showErrorUi(getGeneralErrorText());
210     }
211 
showGeneralWaitingUi()212     private void showGeneralWaitingUi() {
213         mActivationUi.showActivationUi(getUiTitle(), R.string.progress_text, true, 0, 0, 0);
214     }
215 
216     @MainThread
handleInitialEntitlementStatus(@ullable EntitlementResult result)217     private void handleInitialEntitlementStatus(@Nullable EntitlementResult result) {
218         Log.d(TAG, "Initial entitlement result: " + result);
219         if (result == null) {
220             showGeneralErrorUi();
221             finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED);
222             return;
223         }
224         if (isActivationFlow()) {
225             handleEntitlementStatusForActivation(result);
226         } else {
227             handleEntitlementStatusForUpdating(result);
228         }
229     }
230 
231     @MainThread
handleEntitlementStatusForActivation(EntitlementResult result)232     private void handleEntitlementStatusForActivation(EntitlementResult result) {
233         Ts43VowifiStatus vowifiStatus = result.getVowifiStatus();
234         if (vowifiStatus.vowifiEntitled()) {
235             finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL);
236             mActivationUi.setResultAndFinish(Activity.RESULT_OK);
237         } else {
238             if (vowifiStatus.serverDataMissing()) {
239                 if (!TextUtils.isEmpty(result.getTermsAndConditionsWebUrl())) {
240                     mActivationUi.showWebview(
241                             result.getTermsAndConditionsWebUrl(), /* postData= */ null);
242                 } else {
243                     mActivationUi.showWebview(
244                             result.getEmergencyAddressWebUrl(),
245                             result.getEmergencyAddressWebData());
246                 }
247             } else if (vowifiStatus.incompatible()) {
248                 finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__INCOMPATIBLE);
249                 showErrorUi(R.string.failure_contact_carrier);
250             } else {
251                 Log.e(TAG, "Unexpected status. Show error UI.");
252                 finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT);
253                 showGeneralErrorUi();
254             }
255         }
256     }
257 
258     @MainThread
handleEntitlementStatusForUpdating(EntitlementResult result)259     private void handleEntitlementStatusForUpdating(EntitlementResult result) {
260         Ts43VowifiStatus vowifiStatus = result.getVowifiStatus();
261         if (vowifiStatus.vowifiEntitled()) {
262             int launchIntention = ActivityConstants.getLaunchIntention(mStartIntent);
263             if (launchIntention == ActivityConstants.LAUNCH_APP_SHOW_TC) {
264                 mActivationUi.showWebview(
265                         result.getTermsAndConditionsWebUrl(), /* postData= */ null);
266             } else {
267                 mActivationUi.showWebview(
268                         result.getEmergencyAddressWebUrl(), result.getEmergencyAddressWebData());
269             }
270         } else {
271             if (vowifiStatus.incompatible()) {
272                 showErrorUi(R.string.failure_contact_carrier);
273                 mImsUtils.turnOffWfc(
274                         () -> finishStatsLog(
275                                 IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__INCOMPATIBLE)
276                 );
277             } else {
278                 Log.e(TAG, "Unexpected status. Show error UI.");
279                 finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT);
280                 showGeneralErrorUi();
281             }
282         }
283     }
284 
285     @MainThread
handleReevaluationEntitlementStatus(@ullable EntitlementResult result)286     private void handleReevaluationEntitlementStatus(@Nullable EntitlementResult result) {
287         Log.d(TAG, "Reevaluation entitlement result: " + result);
288         if (result == null) { // Network issue
289             showGeneralErrorUi();
290             finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED);
291             return;
292         }
293         if (isActivationFlow()) {
294             handleEntitlementStatusAfterActivation(result);
295         } else {
296             handleEntitlementStatusAfterUpdating(result);
297         }
298     }
299 
300     @MainThread
handleEntitlementStatusAfterActivation(EntitlementResult result)301     private void handleEntitlementStatusAfterActivation(EntitlementResult result) {
302         Ts43VowifiStatus vowifiStatus = result.getVowifiStatus();
303         if (vowifiStatus.vowifiEntitled()) {
304             mActivationUi.setResultAndFinish(Activity.RESULT_OK);
305             finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL);
306         } else {
307             if (vowifiStatus.serverDataMissing()) {
308                 // Check again after 5s, max retry 6 times
309                 if (mEvaluateTimes < ENTITLEMENT_STATUS_UPDATE_RETRY_MAX) {
310                     mEvaluateTimes += 1;
311                     postDelay(
312                             getEntitlementStatusUpdateRetryIntervalMs(),
313                             this::reevaluateEntitlementStatus);
314                 } else {
315                     mEvaluateTimes = 0;
316                     showGeneralErrorUi();
317                     finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__TIMEOUT);
318                 }
319             } else {
320                 // These should never happen, but nothing else we can do. Show general error.
321                 Log.e(TAG, "Unexpected status. Show error UI.");
322                 showGeneralErrorUi();
323                 finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT);
324             }
325         }
326     }
327 
getEntitlementStatusUpdateRetryIntervalMs()328     private long getEntitlementStatusUpdateRetryIntervalMs() {
329         return ENTITLEMENT_STATUS_UPDATE_RETRY_INTERVAL_MS;
330     }
331 
332     @MainThread
handleEntitlementStatusAfterUpdating(EntitlementResult result)333     private void handleEntitlementStatusAfterUpdating(EntitlementResult result) {
334         Ts43VowifiStatus vowifiStatus = result.getVowifiStatus();
335         if (vowifiStatus.vowifiEntitled()) {
336             mActivationUi.setResultAndFinish(Activity.RESULT_OK);
337             finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL);
338         } else if (vowifiStatus.serverDataMissing()) {
339             // Some carrier allows de-activating in updating flow.
340             mImsUtils.turnOffWfc(
341                     () -> {
342                         finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED);
343                         mActivationUi.setResultAndFinish(Activity.RESULT_OK);
344                     });
345         } else {
346             Log.e(TAG, "Unexpected status. Show error UI.");
347             showGeneralErrorUi();
348             finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT);
349         }
350     }
351 
352     /** Runs {@code action} on caller's thread after {@code delayMillis} ms. */
postDelay(long delayMillis, Runnable action)353     private static void postDelay(long delayMillis, Runnable action) {
354         new CountDownTimer(delayMillis, delayMillis + 100) {
355             // Use a countDownInterval bigger than millisInFuture so onTick never fires.
356             @Override
357             public void onTick(long millisUntilFinished) {
358                 // Do nothing
359             }
360 
361             @Override
362             public void onFinish() {
363                 action.run();
364             }
365         }.start();
366     }
367 
finishStatsLog(int result)368     private void finishStatsLog(int result) {
369         mAppResult = result;
370     }
371 }
372