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