1 /* 2 * Copyright 2016, 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.managedprovisioning.preprovisioning; 18 19 import static android.content.res.Configuration.UI_MODE_NIGHT_MASK; 20 import static android.content.res.Configuration.UI_MODE_NIGHT_YES; 21 import static android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED; 22 23 import static com.android.managedprovisioning.model.ProvisioningParams.FLOW_TYPE_LEGACY; 24 import static com.android.managedprovisioning.preprovisioning.PreProvisioningViewModel.STATE_PREPROVISIONING_INITIALIZING; 25 import static com.android.managedprovisioning.preprovisioning.PreProvisioningViewModel.STATE_SHOWING_USER_CONSENT; 26 import static com.android.managedprovisioning.provisioning.Constants.PROVISIONING_SERVICE_INTENT; 27 28 import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SETUP_FLOW; 29 30 import android.app.Activity; 31 import android.app.DialogFragment; 32 import android.content.ComponentName; 33 import android.content.Intent; 34 import android.os.Bundle; 35 import android.provider.Settings; 36 import android.view.ContextMenu; 37 import android.view.ContextMenu.ContextMenuInfo; 38 import android.view.View; 39 import android.widget.TextView; 40 41 import androidx.annotation.VisibleForTesting; 42 43 import com.android.managedprovisioning.ManagedProvisioningScreens; 44 import com.android.managedprovisioning.R; 45 import com.android.managedprovisioning.analytics.MetricsWriterFactory; 46 import com.android.managedprovisioning.analytics.ProvisioningAnalyticsTracker; 47 import com.android.managedprovisioning.common.AccessibilityContextMenuMaker; 48 import com.android.managedprovisioning.common.GetProvisioningModeUtils; 49 import com.android.managedprovisioning.common.Globals; 50 import com.android.managedprovisioning.common.LogoUtils; 51 import com.android.managedprovisioning.common.ManagedProvisioningSharedPreferences; 52 import com.android.managedprovisioning.common.ProvisionLogger; 53 import com.android.managedprovisioning.common.SettingsFacade; 54 import com.android.managedprovisioning.common.SetupGlifLayoutActivity; 55 import com.android.managedprovisioning.common.SimpleDialog; 56 import com.android.managedprovisioning.common.ThemeHelper; 57 import com.android.managedprovisioning.common.ThemeHelper.DefaultNightModeChecker; 58 import com.android.managedprovisioning.common.ThemeHelper.DefaultSetupWizardBridge; 59 import com.android.managedprovisioning.common.Utils; 60 import com.android.managedprovisioning.model.ProvisioningParams; 61 import com.android.managedprovisioning.preprovisioning.PreProvisioningActivityController.UiParams; 62 import com.android.managedprovisioning.provisioning.AdminIntegratedFlowPrepareActivity; 63 import com.android.managedprovisioning.provisioning.ProvisioningActivity; 64 import com.android.managedprovisioning.provisioning.ProvisioningService; 65 66 import com.google.android.setupcompat.util.WizardManagerHelper; 67 68 public class PreProvisioningActivity extends SetupGlifLayoutActivity implements 69 SimpleDialog.SimpleDialogListener, PreProvisioningActivityController.Ui { 70 71 private static final int ENCRYPT_DEVICE_REQUEST_CODE = 1; 72 @VisibleForTesting 73 protected static final int PROVISIONING_REQUEST_CODE = 2; 74 private static final int WIFI_REQUEST_CODE = 3; 75 private static final int CHANGE_LAUNCHER_REQUEST_CODE = 4; 76 private static final int ORGANIZATION_OWNED_LANDING_PAGE_REQUEST_CODE = 5; 77 private static final int GET_PROVISIONING_MODE_REQUEST_CODE = 6; 78 private static final int FINANCED_DEVICE_PREPARE_REQUEST_CODE = 7; 79 private static final int ADMIN_INTEGRATED_FLOW_PREPARE_REQUEST_CODE = 8; 80 81 // Note: must match the constant defined in HomeSettings 82 private static final String EXTRA_SUPPORT_MANAGED_PROFILES = "support_managed_profiles"; 83 84 private static final String ERROR_AND_CLOSE_DIALOG = "PreProvErrorAndCloseDialog"; 85 private static final String BACK_PRESSED_DIALOG_RESET = "PreProvBackPressedDialogReset"; 86 private static final String BACK_PRESSED_DIALOG_CLOSE_ACTIVITY = 87 "PreProvBackPressedDialogCloseActivity"; 88 private static final String LAUNCHER_INVALID_DIALOG = "PreProvCurrentLauncherInvalidDialog"; 89 90 private PreProvisioningActivityController mController; 91 private ControllerProvider mControllerProvider; 92 private final AccessibilityContextMenuMaker mContextMenuMaker; 93 private PreProvisioningActivityBridge mBridge; 94 private boolean mShouldForwardTransition; 95 96 private static final String ERROR_DIALOG_RESET = "ErrorDialogReset"; 97 PreProvisioningActivity()98 public PreProvisioningActivity() { 99 this(activity -> 100 new PreProvisioningActivityController(activity, activity), 101 null, 102 new Utils(), 103 new SettingsFacade(), 104 new ThemeHelper( 105 new DefaultNightModeChecker(), 106 new DefaultSetupWizardBridge())); 107 } 108 109 @VisibleForTesting PreProvisioningActivity( ControllerProvider controllerProvider, AccessibilityContextMenuMaker contextMenuMaker, Utils utils, SettingsFacade settingsFacade, ThemeHelper themeHelper)110 public PreProvisioningActivity( 111 ControllerProvider controllerProvider, 112 AccessibilityContextMenuMaker contextMenuMaker, Utils utils, 113 SettingsFacade settingsFacade, ThemeHelper themeHelper) { 114 super(utils, settingsFacade, themeHelper); 115 mControllerProvider = controllerProvider; 116 mContextMenuMaker = 117 contextMenuMaker != null ? contextMenuMaker : new AccessibilityContextMenuMaker( 118 this); 119 } 120 121 @Override onCreate(Bundle savedInstanceState)122 protected void onCreate(Bundle savedInstanceState) { 123 if (savedInstanceState == null) { 124 getApplicationContext().startService(PROVISIONING_SERVICE_INTENT); 125 } 126 // TODO(b/192074477): Remove deferred setup-specific logic after the managed account flow 127 // starts ManagedProvisioning with the isSetupFlow extra 128 // TODO(b/178822333): Remove NFC-specific logic after adding support for the 129 // admin-integrated flow 130 // This temporary fix only works when called before super.onCreate 131 if (mSettingsFacade.isDeferredSetup(getApplicationContext()) || isNfcSetup()) { 132 getIntent().putExtra(EXTRA_IS_SETUP_FLOW, true); 133 } 134 135 super.onCreate(savedInstanceState); 136 mController = mControllerProvider.getInstance(this); 137 mBridge = createBridge(); 138 mController.getState().observe(this, this::onStateChanged); 139 logMetrics(); 140 } 141 isNfcSetup()142 private boolean isNfcSetup() { 143 return ACTION_NDEF_DISCOVERED.equals(getIntent().getAction()); 144 } 145 146 @Override onResume()147 protected void onResume() { 148 super.onResume(); 149 if (mShouldForwardTransition) { 150 overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); 151 mShouldForwardTransition = false; 152 } 153 } 154 createBridge()155 protected PreProvisioningActivityBridge createBridge() { 156 return new PreProvisioningActivityBridgeImpl( 157 /* activity= */ this, 158 mUtils, 159 PreProvisioningActivity.this::initializeLayoutParams, 160 createBridgeCallbacks(), 161 getThemeHelper()); 162 } 163 createBridgeCallbacks()164 protected final PreProvisioningActivityBridgeCallbacks createBridgeCallbacks() { 165 return new PreProvisioningActivityBridgeCallbacks() { 166 @Override 167 public void onTermsAccepted() { 168 mController.continueProvisioningAfterUserConsent(); 169 } 170 171 @Override 172 public void onTermsButtonClicked() { 173 getTransitionHelper() 174 .startActivityWithTransition(PreProvisioningActivity.this, 175 mController.createViewTermsIntent()); 176 } 177 }; 178 } 179 180 private void onStateChanged(Integer state) { 181 switch (state) { 182 case STATE_PREPROVISIONING_INITIALIZING: 183 mController.initiateProvisioning(getIntent(), getCallingPackage()); 184 break; 185 case STATE_SHOWING_USER_CONSENT: 186 mController.showUserConsentScreen(); 187 break; 188 } 189 } 190 191 @Override 192 public void finish() { 193 // The user has backed out of provisioning, so we perform the necessary clean up steps. 194 LogoUtils.cleanUp(this); 195 ProvisioningParams params = mController.getParams(); 196 if (params != null) { 197 params.cleanUp(); 198 } 199 getEncryptionController().cancelEncryptionReminder(); 200 getApplicationContext().stopService(PROVISIONING_SERVICE_INTENT); 201 super.finish(); 202 } 203 204 @Override 205 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 206 switch (requestCode) { 207 case ENCRYPT_DEVICE_REQUEST_CODE: 208 if (resultCode == RESULT_CANCELED) { 209 ProvisionLogger.loge("User canceled device encryption."); 210 } 211 break; 212 case PROVISIONING_REQUEST_CODE: 213 mController.onReturnFromProvisioning(); 214 setResult(resultCode); 215 getTransitionHelper().finishActivity(this); 216 break; 217 case CHANGE_LAUNCHER_REQUEST_CODE: 218 mController.continueProvisioningAfterUserConsent(); 219 break; 220 case WIFI_REQUEST_CODE: 221 if (resultCode == RESULT_CANCELED) { 222 ProvisionLogger.loge("User canceled wifi picking."); 223 setResult(resultCode); 224 getTransitionHelper().finishActivity(this); 225 } else { 226 if (resultCode == RESULT_OK) { 227 ProvisionLogger.logd("Wifi request result is OK"); 228 } 229 mController.initiateProvisioning(getIntent(), getCallingPackage()); 230 } 231 break; 232 case ORGANIZATION_OWNED_LANDING_PAGE_REQUEST_CODE: 233 case ADMIN_INTEGRATED_FLOW_PREPARE_REQUEST_CODE: 234 if (resultCode == RESULT_OK) { 235 // TODO(b/177849035): Remove NFC-specific logic 236 if (mController.getParams().isNfc) { 237 mController.startNfcFlow(); 238 } else { 239 handleAdminIntegratedFlowPreparerResult(); 240 } 241 } else { 242 ProvisionLogger.loge( 243 "Provisioning was aborted in the preparation stage, " 244 + "requestCode = " + requestCode); 245 if (isDpcInstalled() 246 && mUtils.isOrganizationOwnedAllowed(mController.getParams())) { 247 showFactoryResetDialog(R.string.cant_set_up_device, 248 R.string.contact_your_admin_for_help); 249 } else { 250 showErrorAndClose( 251 R.string.cant_set_up_device, 252 R.string.contact_your_admin_for_help, 253 "Failed provisioning device."); 254 } 255 } 256 break; 257 case GET_PROVISIONING_MODE_REQUEST_CODE: 258 mShouldForwardTransition = true; 259 if (resultCode == RESULT_OK) { 260 if(data != null && mController.updateProvisioningParamsFromIntent(data)) { 261 mController.showUserConsentScreen(); 262 } else { 263 ProvisionLogger.loge( 264 "Invalid data object returned from GET_PROVISIONING_MODE."); 265 if (mUtils.isOrganizationOwnedAllowed(mController.getParams())) { 266 showFactoryResetDialog(R.string.cant_set_up_device, 267 R.string.contact_your_admin_for_help); 268 } else { 269 showErrorAndClose( 270 R.string.cant_set_up_device, 271 R.string.contact_your_admin_for_help, 272 "Failed provisioning personally-owned device."); 273 } 274 } 275 } else { 276 ProvisionLogger.loge("Invalid result code from GET_PROVISIONING_MODE. Expected " 277 + RESULT_OK + " but got " + resultCode + "."); 278 if (mUtils.isOrganizationOwnedAllowed(mController.getParams())) { 279 showFactoryResetDialog(R.string.cant_set_up_device, 280 R.string.contact_your_admin_for_help); 281 } else { 282 showErrorAndClose( 283 R.string.cant_set_up_device, 284 R.string.contact_your_admin_for_help, 285 "Failed to provision personally-owned device."); 286 } 287 } 288 break; 289 case FINANCED_DEVICE_PREPARE_REQUEST_CODE: 290 if (resultCode == RESULT_OK) { 291 startFinancedDeviceFlow(); 292 } else { 293 setResult(resultCode); 294 getTransitionHelper().finishActivity(this); 295 } 296 break; 297 default: 298 ProvisionLogger.logw("Unknown result code :" + resultCode); 299 break; 300 } 301 } 302 303 private boolean isDpcInstalled() { 304 String adminPackageName = mController.getParams().inferDeviceAdminPackageName(); 305 return mUtils.isPackageInstalled(adminPackageName, getPackageManager()); 306 } 307 308 private void handleAdminIntegratedFlowPreparerResult() { 309 if (isDpcInstalled()) { 310 startAdminIntegratedFlowPostDpcInstall(); 311 } else { 312 String adminPackageName = mController.getParams().inferDeviceAdminPackageName(); 313 showErrorAndClose( 314 R.string.cant_set_up_device, 315 R.string.contact_your_admin_for_help, 316 "Package name " + adminPackageName + " is not installed."); 317 } 318 } 319 320 @Override 321 public void showErrorAndClose(Integer titleId, int messageId, String logText) { 322 ProvisionLogger.loge(logText); 323 324 SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder() 325 .setTitle(titleId) 326 .setMessage(messageId) 327 .setCancelable(false) 328 .setPositiveButtonMessage(R.string.device_owner_error_ok); 329 showDialog(dialogBuilder, ERROR_AND_CLOSE_DIALOG); 330 } 331 332 @Override 333 public void onNegativeButtonClick(DialogFragment dialog) { 334 switch (dialog.getTag()) { 335 case BACK_PRESSED_DIALOG_CLOSE_ACTIVITY: 336 case BACK_PRESSED_DIALOG_RESET: 337 // user chose to continue. Do nothing 338 break; 339 case LAUNCHER_INVALID_DIALOG: 340 dialog.dismiss(); 341 break; 342 default: 343 SimpleDialog.throwButtonClickHandlerNotImplemented(dialog); 344 } 345 } 346 347 @Override 348 public void onPositiveButtonClick(DialogFragment dialog) { 349 switch (dialog.getTag()) { 350 case ERROR_AND_CLOSE_DIALOG: 351 case BACK_PRESSED_DIALOG_CLOSE_ACTIVITY: 352 onProvisioningAborted(); 353 break; 354 case BACK_PRESSED_DIALOG_RESET: 355 mUtils.factoryReset(this, "Provisioning cancelled by user on consent screen"); 356 onProvisioningAborted(); 357 break; 358 case LAUNCHER_INVALID_DIALOG: 359 requestLauncherPick(); 360 break; 361 case ERROR_DIALOG_RESET: 362 getUtils().factoryReset(this, "Error during preprovisioning"); 363 setResult(Activity.RESULT_CANCELED); 364 getTransitionHelper().finishActivity(this); 365 break; 366 default: 367 SimpleDialog.throwButtonClickHandlerNotImplemented(dialog); 368 } 369 } 370 371 private void onProvisioningAborted() { 372 setResult(Activity.RESULT_CANCELED); 373 mController.logPreProvisioningCancelled(); 374 getTransitionHelper().finishActivity(this); 375 } 376 377 @Override 378 public void requestEncryption(ProvisioningParams params) { 379 Intent encryptIntent = new Intent(this, 380 getActivityForScreen(ManagedProvisioningScreens.ENCRYPT)); 381 WizardManagerHelper.copyWizardManagerExtras(getIntent(), encryptIntent); 382 encryptIntent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 383 getTransitionHelper().startActivityForResultWithTransition( 384 this, encryptIntent, ENCRYPT_DEVICE_REQUEST_CODE); 385 } 386 387 @Override 388 public void requestWifiPick() { 389 final Intent intent = mUtils.getWifiPickIntent(); 390 WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); 391 getTransitionHelper() 392 .startActivityForResultWithTransition(this, intent, WIFI_REQUEST_CODE); 393 } 394 395 @Override 396 public void showCurrentLauncherInvalid() { 397 SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder() 398 .setCancelable(false) 399 .setTitle(R.string.change_device_launcher) 400 .setMessage(R.string.launcher_app_cant_be_used_by_work_profile) 401 .setNegativeButtonMessage(R.string.cancel_provisioning) 402 .setPositiveButtonMessage(R.string.pick_launcher); 403 showDialog(dialogBuilder, LAUNCHER_INVALID_DIALOG); 404 } 405 406 @Override 407 public void abortProvisioning() { 408 onProvisioningAborted(); 409 } 410 411 @Override 412 public void prepareAdminIntegratedFlow(ProvisioningParams params) { 413 if (AdminIntegratedFlowPrepareActivity.shouldRunPrepareActivity(mUtils, this, params)) { 414 Intent intent = new Intent(this, 415 getActivityForScreen(ManagedProvisioningScreens.ADMIN_INTEGRATED_PREPARE)); 416 WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); 417 intent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 418 getTransitionHelper().startActivityForResultWithTransition( 419 this, intent, ADMIN_INTEGRATED_FLOW_PREPARE_REQUEST_CODE); 420 } else { 421 handleAdminIntegratedFlowPreparerResult(); 422 } 423 } 424 425 private void requestLauncherPick() { 426 Intent changeLauncherIntent = new Intent(Settings.ACTION_HOME_SETTINGS); 427 changeLauncherIntent.putExtra(EXTRA_SUPPORT_MANAGED_PROFILES, true); 428 getTransitionHelper().startActivityForResultWithTransition( 429 this, changeLauncherIntent, CHANGE_LAUNCHER_REQUEST_CODE); 430 } 431 432 /** 433 * Starts {@link ProvisioningActivity}. 434 */ 435 public void startProvisioning(ProvisioningParams params) { 436 Intent intent = new Intent(this, 437 getActivityForScreen(ManagedProvisioningScreens.PROVISIONING)); 438 WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); 439 intent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 440 getTransitionHelper().startActivityForResultWithTransition( 441 this, intent, PROVISIONING_REQUEST_CODE); 442 } 443 444 // TODO: The below group of methods do not belong in the activity. 445 // Move them to the controller instead. 446 /** 447 * Starts either the admin-integrated or the legacy flow, depending on the device state and 448 * DPC capabilities. 449 */ 450 private void startAdminIntegratedFlowPostDpcInstall() { 451 boolean canPerformAdminIntegratedFlow = mUtils.canPerformAdminIntegratedFlow( 452 this, 453 mController.getParams(), 454 mController.getPolicyComplianceUtils(), 455 mController.getGetProvisioningModeUtils()); 456 if (canPerformAdminIntegratedFlow) { 457 startAdminIntegratedFlowWithoutPredeterminedMode(); 458 } else { 459 ProvisionLogger.loge("The admin app does not have handlers for both " 460 + "ACTION_GET_PROVISIONING_MODE and ACTION_ADMIN_POLICY_COMPLIANCE " 461 + "intent actions."); 462 if (mUtils.isOrganizationOwnedAllowed(mController.getParams())) { 463 showFactoryResetDialog(R.string.cant_set_up_device, 464 R.string.contact_your_admin_for_help); 465 } else { 466 showErrorAndClose( 467 R.string.cant_set_up_device, 468 R.string.contact_your_admin_for_help, 469 "Failed provisioning personally-owned device."); 470 } 471 } 472 mController.logProvisioningFlowType(); 473 } 474 475 private void startAdminIntegratedFlowWithoutPredeterminedMode() { 476 ProvisionLogger.logi("Starting the admin-integrated flow."); 477 GetProvisioningModeUtils provisioningModeUtils = mController.getGetProvisioningModeUtils(); 478 Bundle additionalExtras = mController.getAdditionalExtrasForGetProvisioningModeIntent(); 479 provisioningModeUtils.startGetProvisioningModeActivityIfResolved( 480 this, mController.getParams(), additionalExtras, 481 GET_PROVISIONING_MODE_REQUEST_CODE, getTransitionHelper()); 482 } 483 484 private void startFinancedDeviceFlow() { 485 ProvisionLogger.logi("Starting the financed device flow."); 486 mController.updateProvisioningFlowState(FLOW_TYPE_LEGACY); 487 mController.continueProvisioningAfterUserConsent(); 488 } 489 490 @Override 491 public void showFactoryResetDialog(Integer titleId, int messageId) { 492 SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder() 493 .setTitle(titleId) 494 .setMessage(messageId) 495 .setCancelable(false) 496 .setPositiveButtonMessage(R.string.reset); 497 498 showDialog(dialogBuilder, ERROR_DIALOG_RESET); 499 } 500 501 @Override 502 public void initiateUi(UiParams uiParams) { 503 mBridge.initiateUi(uiParams); 504 } 505 506 @Override 507 public void showOwnershipDisclaimerScreen(ProvisioningParams params) { 508 Intent intent = new Intent(this, 509 getActivityForScreen(ManagedProvisioningScreens.LANDING)); 510 WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); 511 intent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 512 getTransitionHelper().startActivityForResultWithTransition( 513 this, intent, ORGANIZATION_OWNED_LANDING_PAGE_REQUEST_CODE); 514 } 515 516 @Override 517 public void prepareFinancedDeviceFlow(ProvisioningParams params) { 518 Intent intent = new Intent(this, 519 getActivityForScreen(ManagedProvisioningScreens.FINANCED_DEVICE_LANDING)); 520 WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); 521 intent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 522 getTransitionHelper().startActivityForResultWithTransition( 523 this, intent, FINANCED_DEVICE_PREPARE_REQUEST_CODE); 524 } 525 526 @Override 527 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 528 super.onCreateContextMenu(menu, v, menuInfo); 529 if (v instanceof TextView) { 530 mContextMenuMaker.populateMenuContent(menu, (TextView) v); 531 } 532 } 533 534 @Override 535 public void onBackPressed() { 536 if (mUtils.isOrganizationOwnedAllowed(mController.getParams())) { 537 showDialog(mUtils.createCancelProvisioningResetDialogBuilder(), 538 BACK_PRESSED_DIALOG_RESET); 539 } else { 540 showDialog(mUtils.createCancelProvisioningDialogBuilder(), 541 BACK_PRESSED_DIALOG_CLOSE_ACTIVITY); 542 } 543 } 544 545 private void logMetrics() { 546 final ProvisioningAnalyticsTracker analyticsTracker = 547 new ProvisioningAnalyticsTracker( 548 MetricsWriterFactory.getMetricsWriter(this, new SettingsFacade()), 549 new ManagedProvisioningSharedPreferences(this)); 550 int nightMode = getResources().getConfiguration().uiMode & UI_MODE_NIGHT_MASK; 551 analyticsTracker.logIsNightMode(nightMode == UI_MODE_NIGHT_YES); 552 } 553 554 /** 555 * Constructs {@link PreProvisioningActivityController} for a given {@link 556 * PreProvisioningActivity} 557 */ 558 interface ControllerProvider { 559 /** 560 * Constructs {@link PreProvisioningActivityController} for a given {@link 561 * PreProvisioningActivity} 562 */ 563 PreProvisioningActivityController getInstance(PreProvisioningActivity activity); 564 } 565 } 566