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