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 17package com.android.managedprovisioning.preprovisioning; 18 19import static java.util.Collections.emptyList; 20import static java.util.Collections.unmodifiableList; 21 22import android.annotation.NonNull; 23import android.app.Activity; 24import android.app.DialogFragment; 25import android.content.ComponentName; 26import android.content.Intent; 27import android.content.res.ColorStateList; 28import android.graphics.drawable.Drawable; 29import android.os.Bundle; 30import android.os.UserHandle; 31import android.provider.Settings; 32import android.support.annotation.VisibleForTesting; 33import android.text.Spannable; 34import android.text.SpannableString; 35import android.text.Spanned; 36import android.text.TextUtils; 37import android.text.method.LinkMovementMethod; 38import android.text.style.ClickableSpan; 39import android.view.ContextMenu; 40import android.view.ContextMenu.ContextMenuInfo; 41import android.view.View; 42import android.widget.Button; 43import android.widget.ImageView; 44import android.widget.TextView; 45 46import com.android.managedprovisioning.R; 47import com.android.managedprovisioning.common.AccessibilityContextMenuMaker; 48import com.android.managedprovisioning.common.ClickableSpanFactory; 49import com.android.managedprovisioning.common.LogoUtils; 50import com.android.managedprovisioning.common.ProvisionLogger; 51import com.android.managedprovisioning.common.SetupGlifLayoutActivity; 52import com.android.managedprovisioning.common.SimpleDialog; 53import com.android.managedprovisioning.common.StringConcatenator; 54import com.android.managedprovisioning.common.TouchTargetEnforcer; 55import com.android.managedprovisioning.common.Utils; 56import com.android.managedprovisioning.model.CustomizationParams; 57import com.android.managedprovisioning.model.ProvisioningParams; 58import com.android.managedprovisioning.preprovisioning.anim.BenefitsAnimation; 59import com.android.managedprovisioning.preprovisioning.anim.ColorMatcher; 60import com.android.managedprovisioning.preprovisioning.anim.SwiperThemeMatcher; 61import com.android.managedprovisioning.preprovisioning.terms.TermsActivity; 62import com.android.managedprovisioning.provisioning.ProvisioningActivity; 63 64import java.util.ArrayList; 65import java.util.List; 66 67public class PreProvisioningActivity extends SetupGlifLayoutActivity implements 68 SimpleDialog.SimpleDialogListener, PreProvisioningController.Ui { 69 private static final List<Integer> SLIDE_CAPTIONS = createImmutableList( 70 R.string.info_anim_title_0, 71 R.string.info_anim_title_1, 72 R.string.info_anim_title_2); 73 private static final List<Integer> SLIDE_CAPTIONS_COMP = createImmutableList( 74 R.string.info_anim_title_0, 75 R.string.one_place_for_work_apps, 76 R.string.info_anim_title_2); 77 78 private static final int ENCRYPT_DEVICE_REQUEST_CODE = 1; 79 @VisibleForTesting 80 protected static final int PROVISIONING_REQUEST_CODE = 2; 81 private static final int WIFI_REQUEST_CODE = 3; 82 private static final int CHANGE_LAUNCHER_REQUEST_CODE = 4; 83 84 // Note: must match the constant defined in HomeSettings 85 private static final String EXTRA_SUPPORT_MANAGED_PROFILES = "support_managed_profiles"; 86 private static final String SAVED_PROVISIONING_PARAMS = "saved_provisioning_params"; 87 88 private static final String ERROR_AND_CLOSE_DIALOG = "PreProvErrorAndCloseDialog"; 89 private static final String BACK_PRESSED_DIALOG = "PreProvBackPressedDialog"; 90 private static final String CANCELLED_CONSENT_DIALOG = "PreProvCancelledConsentDialog"; 91 private static final String LAUNCHER_INVALID_DIALOG = "PreProvCurrentLauncherInvalidDialog"; 92 private static final String DELETE_MANAGED_PROFILE_DIALOG = "PreProvDeleteManagedProfileDialog"; 93 94 private PreProvisioningController mController; 95 private ControllerProvider mControllerProvider; 96 private final AccessibilityContextMenuMaker mContextMenuMaker; 97 private BenefitsAnimation mBenefitsAnimation; 98 private ClickableSpanFactory mClickableSpanFactory; 99 private TouchTargetEnforcer mTouchTargetEnforcer; 100 101 public PreProvisioningActivity() { 102 this(activity -> new PreProvisioningController(activity, activity), null, new Utils()); 103 } 104 105 @VisibleForTesting 106 public PreProvisioningActivity(ControllerProvider controllerProvider, 107 AccessibilityContextMenuMaker contextMenuMaker, 108 Utils utils) { 109 super(utils); 110 mControllerProvider = controllerProvider; 111 mContextMenuMaker = 112 contextMenuMaker != null ? contextMenuMaker : new AccessibilityContextMenuMaker( 113 this); 114 } 115 116 @Override 117 protected void onCreate(Bundle savedInstanceState) { 118 super.onCreate(savedInstanceState); 119 mClickableSpanFactory = new ClickableSpanFactory(getColor(R.color.blue)); 120 mTouchTargetEnforcer = new TouchTargetEnforcer(getResources().getDisplayMetrics().density); 121 mController = mControllerProvider.getInstance(this); 122 ProvisioningParams params = savedInstanceState == null ? null 123 : savedInstanceState.getParcelable(SAVED_PROVISIONING_PARAMS); 124 mController.initiateProvisioning(getIntent(), params, getCallingPackage()); 125 } 126 127 @Override 128 public void finish() { 129 // The user has backed out of provisioning, so we perform the necessary clean up steps. 130 LogoUtils.cleanUp(this); 131 ProvisioningParams params = mController.getParams(); 132 if (params != null) { 133 params.cleanUp(); 134 } 135 EncryptionController.getInstance(this).cancelEncryptionReminder(); 136 super.finish(); 137 } 138 139 @Override 140 protected void onSaveInstanceState(Bundle outState) { 141 super.onSaveInstanceState(outState); 142 outState.putParcelable(SAVED_PROVISIONING_PARAMS, mController.getParams()); 143 } 144 145 @Override 146 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 147 switch (requestCode) { 148 case ENCRYPT_DEVICE_REQUEST_CODE: 149 if (resultCode == RESULT_CANCELED) { 150 ProvisionLogger.loge("User canceled device encryption."); 151 } 152 break; 153 case PROVISIONING_REQUEST_CODE: 154 setResult(resultCode); 155 finish(); 156 break; 157 case CHANGE_LAUNCHER_REQUEST_CODE: 158 mController.continueProvisioningAfterUserConsent(); 159 break; 160 case WIFI_REQUEST_CODE: 161 if (resultCode == RESULT_CANCELED) { 162 ProvisionLogger.loge("User canceled wifi picking."); 163 } else if (resultCode == RESULT_OK) { 164 ProvisionLogger.logd("Wifi request result is OK"); 165 } 166 mController.initiateProvisioning(getIntent(), null /* cached params */, 167 getCallingPackage()); 168 break; 169 default: 170 ProvisionLogger.logw("Unknown result code :" + resultCode); 171 break; 172 } 173 } 174 175 @Override 176 public void showErrorAndClose(Integer titleId, int messageId, String logText) { 177 ProvisionLogger.loge(logText); 178 179 SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder() 180 .setTitle(titleId) 181 .setMessage(messageId) 182 .setCancelable(false) 183 .setPositiveButtonMessage(R.string.device_owner_error_ok); 184 showDialog(dialogBuilder, ERROR_AND_CLOSE_DIALOG); 185 } 186 187 @Override 188 public void onNegativeButtonClick(DialogFragment dialog) { 189 switch (dialog.getTag()) { 190 case CANCELLED_CONSENT_DIALOG: 191 case BACK_PRESSED_DIALOG: 192 // user chose to continue. Do nothing 193 break; 194 case LAUNCHER_INVALID_DIALOG: 195 dialog.dismiss(); 196 break; 197 case DELETE_MANAGED_PROFILE_DIALOG: 198 setResult(Activity.RESULT_CANCELED); 199 finish(); 200 break; 201 default: 202 SimpleDialog.throwButtonClickHandlerNotImplemented(dialog); 203 } 204 } 205 206 @Override 207 public void onPositiveButtonClick(DialogFragment dialog) { 208 switch (dialog.getTag()) { 209 case ERROR_AND_CLOSE_DIALOG: 210 case BACK_PRESSED_DIALOG: 211 // Close activity 212 setResult(Activity.RESULT_CANCELED); 213 // TODO: Move logging to close button, if we finish provisioning there. 214 mController.logPreProvisioningCancelled(); 215 finish(); 216 break; 217 case CANCELLED_CONSENT_DIALOG: 218 mUtils.sendFactoryResetBroadcast(this, "Device owner setup cancelled"); 219 break; 220 case LAUNCHER_INVALID_DIALOG: 221 requestLauncherPick(); 222 break; 223 case DELETE_MANAGED_PROFILE_DIALOG: 224 DeleteManagedProfileDialog d = (DeleteManagedProfileDialog) dialog; 225 mController.removeUser(d.getUserId()); 226 // TODO: refactor as evil - logic should be less spread out 227 // Check if we are in the middle of silent provisioning and were got blocked by an 228 // existing user profile. If so, we can now resume. 229 mController.checkResumeSilentProvisioning(); 230 break; 231 default: 232 SimpleDialog.throwButtonClickHandlerNotImplemented(dialog); 233 } 234 } 235 236 @Override 237 public void requestEncryption(ProvisioningParams params) { 238 Intent encryptIntent = new Intent(this, EncryptDeviceActivity.class); 239 encryptIntent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 240 startActivityForResult(encryptIntent, ENCRYPT_DEVICE_REQUEST_CODE); 241 } 242 243 @Override 244 public void requestWifiPick() { 245 startActivityForResult(mUtils.getWifiPickIntent(), WIFI_REQUEST_CODE); 246 } 247 248 @Override 249 public void showCurrentLauncherInvalid() { 250 SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder() 251 .setCancelable(false) 252 .setTitle(R.string.change_device_launcher) 253 .setMessage(R.string.launcher_app_cant_be_used_by_work_profile) 254 .setNegativeButtonMessage(R.string.cancel_provisioning) 255 .setPositiveButtonMessage(R.string.pick_launcher); 256 showDialog(dialogBuilder, LAUNCHER_INVALID_DIALOG); 257 } 258 259 private void requestLauncherPick() { 260 Intent changeLauncherIntent = new Intent(Settings.ACTION_HOME_SETTINGS); 261 changeLauncherIntent.putExtra(EXTRA_SUPPORT_MANAGED_PROFILES, true); 262 startActivityForResult(changeLauncherIntent, CHANGE_LAUNCHER_REQUEST_CODE); 263 } 264 265 public void startProvisioning(int userId, ProvisioningParams params) { 266 Intent intent = new Intent(this, ProvisioningActivity.class); 267 intent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 268 startActivityForResultAsUser(intent, PROVISIONING_REQUEST_CODE, new UserHandle(userId)); 269 overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); 270 } 271 272 @Override 273 public void initiateUi(int layoutId, int titleId, String packageLabel, Drawable packageIcon, 274 boolean isProfileOwnerProvisioning, boolean isComp, List<String> termsHeaders, 275 CustomizationParams customization) { 276 initializeLayoutParams( 277 layoutId, 278 isProfileOwnerProvisioning ? null : R.string.set_up_your_device, 279 customization.mainColor, 280 customization.statusBarColor); 281 282 // set up the 'accept and continue' button 283 Button nextButton = (Button) findViewById(R.id.next_button); 284 nextButton.setOnClickListener(v -> { 285 ProvisionLogger.logi("Next button (next_button) is clicked."); 286 mController.continueProvisioningAfterUserConsent(); 287 }); 288 nextButton.setBackgroundTintList(ColorStateList.valueOf(customization.mainColor)); 289 if (mUtils.isBrightColor(customization.mainColor)) { 290 nextButton.setTextColor(getColor(R.color.gray_button_text)); 291 } 292 293 // set the activity title 294 setTitle(titleId); 295 296 // set up terms headers 297 String headers = new StringConcatenator(getResources()).join(termsHeaders); 298 299 // initiate UI for MP / DO 300 if (isProfileOwnerProvisioning) { 301 initiateUIProfileOwner(headers, isComp, customization); 302 } else { 303 initiateUIDeviceOwner(packageLabel, packageIcon, headers, customization); 304 } 305 } 306 307 private void initiateUIProfileOwner( 308 @NonNull String termsHeaders, boolean isComp, CustomizationParams customizationParams) { 309 // set up the cancel button 310 Button cancelButton = (Button) findViewById(R.id.close_button); 311 cancelButton.setOnClickListener(v -> { 312 ProvisionLogger.logi("Close button (close_button) is clicked."); 313 PreProvisioningActivity.this.onBackPressed(); 314 }); 315 316 int messageId = isComp ? R.string.profile_owner_info_comp : R.string.profile_owner_info; 317 int messageWithTermsId = isComp ? R.string.profile_owner_info_with_terms_headers_comp 318 : R.string.profile_owner_info_with_terms_headers; 319 320 // set the short info text 321 TextView shortInfo = (TextView) findViewById(R.id.profile_owner_short_info); 322 shortInfo.setText(termsHeaders.isEmpty() 323 ? getString(messageId) 324 : getResources().getString(messageWithTermsId, termsHeaders)); 325 326 // set up show terms button 327 View viewTermsButton = findViewById(R.id.show_terms_button); 328 viewTermsButton.setOnClickListener(this::startViewTermsActivity); 329 mTouchTargetEnforcer.enforce(viewTermsButton, (View) viewTermsButton.getParent()); 330 331 // show the intro animation 332 mBenefitsAnimation = new BenefitsAnimation( 333 this, 334 isComp 335 ? SLIDE_CAPTIONS_COMP 336 : SLIDE_CAPTIONS, 337 isComp 338 ? R.string.comp_profile_benefits_description 339 : R.string.profile_benefits_description, 340 customizationParams); 341 } 342 343 private void initiateUIDeviceOwner(String packageName, Drawable packageIcon, 344 @NonNull String termsHeaders, CustomizationParams customization) { 345 // short terms info text with clickable 'view terms' link 346 TextView shortInfoText = (TextView) findViewById(R.id.device_owner_terms_info); 347 shortInfoText.setText(assembleDOTermsMessage(termsHeaders, customization.orgName)); 348 shortInfoText.setMovementMethod(LinkMovementMethod.getInstance()); // make clicks work 349 mContextMenuMaker.registerWithActivity(shortInfoText); 350 351 // if you have any questions, contact your device's provider 352 // 353 // TODO: refactor complex localized string assembly to an abstraction http://b/34288292 354 // there is a bit of copy-paste, and some details easy to forget (e.g. setMovementMethod) 355 if (customization.supportUrl != null) { 356 TextView info = (TextView) findViewById(R.id.device_owner_provider_info); 357 info.setVisibility(View.VISIBLE); 358 String deviceProvider = getString(R.string.organization_admin); 359 String contactDeviceProvider = getString(R.string.contact_device_provider, 360 deviceProvider); 361 SpannableString spannableString = new SpannableString(contactDeviceProvider); 362 363 Intent intent = WebActivity.createIntent(this, customization.supportUrl, 364 customization.statusBarColor); 365 if (intent != null) { 366 ClickableSpan span = mClickableSpanFactory.create(intent); 367 int startIx = contactDeviceProvider.indexOf(deviceProvider); 368 int endIx = startIx + deviceProvider.length(); 369 spannableString.setSpan(span, startIx, endIx, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 370 info.setMovementMethod(LinkMovementMethod.getInstance()); // make clicks work 371 } 372 373 info.setText(spannableString); 374 mContextMenuMaker.registerWithActivity(info); 375 } 376 377 // set up DPC icon and label 378 setDpcIconAndLabel(packageName, packageIcon, customization.orgName); 379 } 380 381 @Override 382 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 383 super.onCreateContextMenu(menu, v, menuInfo); 384 if (v instanceof TextView) { 385 mContextMenuMaker.populateMenuContent(menu, (TextView) v); 386 } 387 } 388 389 private void startViewTermsActivity(@SuppressWarnings("unused") View view) { 390 startActivity(createViewTermsIntent()); 391 } 392 393 private Intent createViewTermsIntent() { 394 return new Intent(this, TermsActivity.class).putExtra( 395 ProvisioningParams.EXTRA_PROVISIONING_PARAMS, mController.getParams()); 396 } 397 398 // TODO: refactor complex localized string assembly to an abstraction http://b/34288292 399 // there is a bit of copy-paste, and some details easy to forget (e.g. setMovementMethod) 400 private Spannable assembleDOTermsMessage(@NonNull String termsHeaders, String orgName) { 401 String linkText = getString(R.string.view_terms); 402 403 if (TextUtils.isEmpty(orgName)) { 404 orgName = getString(R.string.your_organization_middle); 405 } 406 String messageText = termsHeaders.isEmpty() 407 ? getString(R.string.device_owner_info, orgName, linkText) 408 : getString(R.string.device_owner_info_with_terms_headers, orgName, termsHeaders, 409 linkText); 410 411 Spannable result = new SpannableString(messageText); 412 int start = messageText.indexOf(linkText); 413 414 ClickableSpan span = mClickableSpanFactory.create(createViewTermsIntent()); 415 result.setSpan(span, start, start + linkText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 416 return result; 417 } 418 419 private void setDpcIconAndLabel(@NonNull String appName, Drawable packageIcon, String orgName) { 420 if (packageIcon == null || TextUtils.isEmpty(appName)) { 421 return; 422 } 423 424 // make a container with all parts of DPC app description visible 425 findViewById(R.id.intro_device_owner_app_info_container).setVisibility(View.VISIBLE); 426 427 if (TextUtils.isEmpty(orgName)) { 428 orgName = getString(R.string.your_organization_beginning); 429 } 430 String message = getString(R.string.your_org_app_used, orgName); 431 TextView appInfoText = (TextView) findViewById(R.id.device_owner_app_info_text); 432 appInfoText.setText(message); 433 434 ImageView imageView = (ImageView) findViewById(R.id.device_manager_icon_view); 435 imageView.setImageDrawable(packageIcon); 436 imageView.setContentDescription(getResources().getString(R.string.mdm_icon_label, appName)); 437 438 TextView deviceManagerName = (TextView) findViewById(R.id.device_manager_name); 439 deviceManagerName.setText(appName); 440 } 441 442 @Override 443 public void showDeleteManagedProfileDialog(ComponentName mdmPackageName, String domainName, 444 int userId) { 445 showDialog(() -> DeleteManagedProfileDialog.newInstance(userId, 446 mdmPackageName, domainName), DELETE_MANAGED_PROFILE_DIALOG); 447 } 448 449 @Override 450 public void onBackPressed() { 451 mController.logPreProvisioningCancelled(); 452 super.onBackPressed(); 453 } 454 455 @Override 456 protected void onResume() { 457 super.onResume(); 458 if (mBenefitsAnimation != null) { 459 mBenefitsAnimation.start(); 460 } 461 } 462 463 @Override 464 protected void onPause() { 465 super.onPause(); 466 if (mBenefitsAnimation != null) { 467 mBenefitsAnimation.stop(); 468 } 469 } 470 471 private static List<Integer> createImmutableList(int... values) { 472 if (values == null || values.length == 0) { 473 return emptyList(); 474 } 475 List<Integer> result = new ArrayList<>(values.length); 476 for (int value : values) { 477 result.add(value); 478 } 479 return unmodifiableList(result); 480 } 481 482 /** 483 * Constructs {@link PreProvisioningController} for a given {@link PreProvisioningActivity} 484 */ 485 interface ControllerProvider { 486 /** 487 * Constructs {@link PreProvisioningController} for a given {@link PreProvisioningActivity} 488 */ 489 PreProvisioningController getInstance(PreProvisioningActivity activity); 490 } 491}