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.graphics.drawable.Drawable; 28 import android.os.Bundle; 29 import android.os.UserHandle; 30 import android.provider.Settings; 31 import android.text.Spannable; 32 import android.text.SpannableString; 33 import android.text.Spanned; 34 import android.text.TextUtils; 35 import android.text.method.LinkMovementMethod; 36 import android.text.style.ClickableSpan; 37 import android.view.ContextMenu; 38 import android.view.ContextMenu.ContextMenuInfo; 39 import android.view.View; 40 import android.widget.Button; 41 import android.widget.ImageView; 42 import android.widget.TextView; 43 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.managedprovisioning.R; 46 import com.android.managedprovisioning.common.ClickableSpanFactory; 47 import com.android.managedprovisioning.common.AccessibilityContextMenuMaker; 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 62 import java.util.ArrayList; 63 import java.util.List; 64 65 public class PreProvisioningActivity extends SetupGlifLayoutActivity implements 66 SimpleDialog.SimpleDialogListener, PreProvisioningController.Ui { 67 private static final List<Integer> SLIDE_CAPTIONS = createImmutableList( 68 R.string.info_anim_title_0, 69 R.string.info_anim_title_1, 70 R.string.info_anim_title_2); 71 private static final List<Integer> SLIDE_CAPTIONS_COMP = createImmutableList( 72 R.string.info_anim_title_0, 73 R.string.one_place_for_work_apps, 74 R.string.info_anim_title_2); 75 76 private static final int ENCRYPT_DEVICE_REQUEST_CODE = 1; 77 @VisibleForTesting 78 protected static final int PROVISIONING_REQUEST_CODE = 2; 79 private static final int WIFI_REQUEST_CODE = 3; 80 private static final int CHANGE_LAUNCHER_REQUEST_CODE = 4; 81 82 // Note: must match the constant defined in HomeSettings 83 private static final String EXTRA_SUPPORT_MANAGED_PROFILES = "support_managed_profiles"; 84 private static final String SAVED_PROVISIONING_PARAMS = "saved_provisioning_params"; 85 86 private static final String ERROR_AND_CLOSE_DIALOG = "PreProvErrorAndCloseDialog"; 87 private static final String BACK_PRESSED_DIALOG = "PreProvBackPressedDialog"; 88 private static final String CANCELLED_CONSENT_DIALOG = "PreProvCancelledConsentDialog"; 89 private static final String LAUNCHER_INVALID_DIALOG = "PreProvCurrentLauncherInvalidDialog"; 90 private static final String DELETE_MANAGED_PROFILE_DIALOG = "PreProvDeleteManagedProfileDialog"; 91 92 private PreProvisioningController mController; 93 private ControllerProvider mControllerProvider; 94 private final AccessibilityContextMenuMaker mContextMenuMaker; 95 private BenefitsAnimation mBenefitsAnimation; 96 private ClickableSpanFactory mClickableSpanFactory; 97 private TouchTargetEnforcer mTouchTargetEnforcer; 98 PreProvisioningActivity()99 public PreProvisioningActivity() { 100 this(activity -> new PreProvisioningController(activity, activity), null); 101 } 102 103 @VisibleForTesting PreProvisioningActivity(ControllerProvider controllerProvider, AccessibilityContextMenuMaker contextMenuMaker)104 public PreProvisioningActivity(ControllerProvider controllerProvider, 105 AccessibilityContextMenuMaker contextMenuMaker) { 106 mControllerProvider = controllerProvider; 107 mContextMenuMaker = 108 contextMenuMaker != null ? contextMenuMaker : new AccessibilityContextMenuMaker( 109 this); 110 } 111 112 @Override onCreate(Bundle savedInstanceState)113 protected void onCreate(Bundle savedInstanceState) { 114 super.onCreate(savedInstanceState); 115 mClickableSpanFactory = new ClickableSpanFactory(getColor(R.color.blue)); 116 mTouchTargetEnforcer = new TouchTargetEnforcer(getResources().getDisplayMetrics().density); 117 mController = mControllerProvider.getInstance(this); 118 ProvisioningParams params = savedInstanceState == null ? null 119 : savedInstanceState.getParcelable(SAVED_PROVISIONING_PARAMS); 120 mController.initiateProvisioning(getIntent(), params, getCallingPackage()); 121 } 122 123 @Override finish()124 public void finish() { 125 // The user has backed out of provisioning, so we perform the necessary clean up steps. 126 LogoUtils.cleanUp(this); 127 ProvisioningParams params = mController.getParams(); 128 if (params != null) { 129 params.cleanUp(); 130 } 131 EncryptionController.getInstance(this).cancelEncryptionReminder(); 132 super.finish(); 133 } 134 135 @Override onSaveInstanceState(Bundle outState)136 protected void onSaveInstanceState(Bundle outState) { 137 super.onSaveInstanceState(outState); 138 outState.putParcelable(SAVED_PROVISIONING_PARAMS, mController.getParams()); 139 } 140 141 @Override onActivityResult(int requestCode, int resultCode, Intent data)142 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 143 switch (requestCode) { 144 case ENCRYPT_DEVICE_REQUEST_CODE: 145 if (resultCode == RESULT_CANCELED) { 146 ProvisionLogger.loge("User canceled device encryption."); 147 } 148 break; 149 case PROVISIONING_REQUEST_CODE: 150 setResult(resultCode); 151 finish(); 152 break; 153 case CHANGE_LAUNCHER_REQUEST_CODE: 154 mController.continueProvisioningAfterUserConsent(); 155 break; 156 case WIFI_REQUEST_CODE: 157 if (resultCode == RESULT_CANCELED) { 158 ProvisionLogger.loge("User canceled wifi picking."); 159 } else if (resultCode == RESULT_OK) { 160 ProvisionLogger.logd("Wifi request result is OK"); 161 } 162 mController.initiateProvisioning(getIntent(), null /* cached params */, 163 getCallingPackage()); 164 break; 165 default: 166 ProvisionLogger.logw("Unknown result code :" + resultCode); 167 break; 168 } 169 } 170 171 @Override showErrorAndClose(Integer titleId, int messageId, String logText)172 public void showErrorAndClose(Integer titleId, int messageId, String logText) { 173 ProvisionLogger.loge(logText); 174 175 SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder() 176 .setTitle(titleId) 177 .setMessage(messageId) 178 .setCancelable(false) 179 .setPositiveButtonMessage(R.string.device_owner_error_ok); 180 showDialog(dialogBuilder, ERROR_AND_CLOSE_DIALOG); 181 } 182 183 @Override onNegativeButtonClick(DialogFragment dialog)184 public void onNegativeButtonClick(DialogFragment dialog) { 185 switch (dialog.getTag()) { 186 case CANCELLED_CONSENT_DIALOG: 187 case BACK_PRESSED_DIALOG: 188 // user chose to continue. Do nothing 189 break; 190 case LAUNCHER_INVALID_DIALOG: 191 dialog.dismiss(); 192 break; 193 case DELETE_MANAGED_PROFILE_DIALOG: 194 setResult(Activity.RESULT_CANCELED); 195 finish(); 196 break; 197 default: 198 SimpleDialog.throwButtonClickHandlerNotImplemented(dialog); 199 } 200 } 201 202 @Override onPositiveButtonClick(DialogFragment dialog)203 public void onPositiveButtonClick(DialogFragment dialog) { 204 switch (dialog.getTag()) { 205 case ERROR_AND_CLOSE_DIALOG: 206 case BACK_PRESSED_DIALOG: 207 // Close activity 208 setResult(Activity.RESULT_CANCELED); 209 // TODO: Move logging to close button, if we finish provisioning there. 210 mController.logPreProvisioningCancelled(); 211 finish(); 212 break; 213 case CANCELLED_CONSENT_DIALOG: 214 mUtils.sendFactoryResetBroadcast(this, "Device owner setup cancelled"); 215 break; 216 case LAUNCHER_INVALID_DIALOG: 217 requestLauncherPick(); 218 break; 219 case DELETE_MANAGED_PROFILE_DIALOG: 220 DeleteManagedProfileDialog d = (DeleteManagedProfileDialog) dialog; 221 mController.removeUser(d.getUserId()); 222 // TODO: refactor as evil - logic should be less spread out 223 // Check if we are in the middle of silent provisioning and were got blocked by an 224 // existing user profile. If so, we can now resume. 225 mController.checkResumeSilentProvisioning(); 226 break; 227 default: 228 SimpleDialog.throwButtonClickHandlerNotImplemented(dialog); 229 } 230 } 231 232 @Override requestEncryption(ProvisioningParams params)233 public void requestEncryption(ProvisioningParams params) { 234 Intent encryptIntent = new Intent(this, EncryptDeviceActivity.class); 235 encryptIntent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 236 startActivityForResult(encryptIntent, ENCRYPT_DEVICE_REQUEST_CODE); 237 } 238 239 @Override requestWifiPick()240 public void requestWifiPick() { 241 startActivityForResult(mUtils.getWifiPickIntent(), WIFI_REQUEST_CODE); 242 } 243 244 @Override showCurrentLauncherInvalid()245 public void showCurrentLauncherInvalid() { 246 SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder() 247 .setCancelable(false) 248 .setTitle(R.string.change_device_launcher) 249 .setMessage(R.string.launcher_app_cant_be_used_by_work_profile) 250 .setNegativeButtonMessage(R.string.cancel_provisioning) 251 .setPositiveButtonMessage(R.string.pick_launcher); 252 showDialog(dialogBuilder, LAUNCHER_INVALID_DIALOG); 253 } 254 requestLauncherPick()255 private void requestLauncherPick() { 256 Intent changeLauncherIntent = new Intent(Settings.ACTION_HOME_SETTINGS); 257 changeLauncherIntent.putExtra(EXTRA_SUPPORT_MANAGED_PROFILES, true); 258 startActivityForResult(changeLauncherIntent, CHANGE_LAUNCHER_REQUEST_CODE); 259 } 260 startProvisioning(int userId, ProvisioningParams params)261 public void startProvisioning(int userId, ProvisioningParams params) { 262 Intent intent = new Intent(this, ProvisioningActivity.class); 263 intent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params); 264 startActivityForResultAsUser(intent, PROVISIONING_REQUEST_CODE, new UserHandle(userId)); 265 overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); 266 } 267 268 @Override initiateUi(int layoutId, int titleId, String packageLabel, Drawable packageIcon, boolean isProfileOwnerProvisioning, boolean isComp, List<String> termsHeaders, CustomizationParams customization)269 public void initiateUi(int layoutId, int titleId, String packageLabel, Drawable packageIcon, 270 boolean isProfileOwnerProvisioning, boolean isComp, List<String> termsHeaders, 271 CustomizationParams customization) { 272 if (isProfileOwnerProvisioning) { 273 // setting a theme so that the animation swiper matches the mainColor 274 // needs to happen before {@link Activity#setContentView} 275 setTheme(new SwiperThemeMatcher(this, 276 new ColorMatcher()) // TODO: introduce DI framework 277 .findTheme(customization.swiperColor)); 278 } 279 280 initializeLayoutParams( 281 layoutId, 282 isProfileOwnerProvisioning ? null : R.string.set_up_your_device, 283 false /* progress bar */, 284 customization.statusBarColor); 285 286 // set up the 'accept and continue' button 287 Button nextButton = (Button) findViewById(R.id.next_button); 288 nextButton.setOnClickListener(v -> { 289 ProvisionLogger.logi("Next button (next_button) is clicked."); 290 mController.continueProvisioningAfterUserConsent(); 291 }); 292 nextButton.setBackgroundColor(customization.buttonColor); 293 if (mUtils.isBrightColor(customization.buttonColor)) { 294 nextButton.setTextColor(getColor(R.color.gray_button_text)); 295 } 296 297 // set the activity title 298 setTitle(titleId); 299 300 // set up terms headers 301 String headers = new StringConcatenator(getResources()).join(termsHeaders); 302 303 // initiate UI for MP / DO 304 if (isProfileOwnerProvisioning) { 305 initiateUIProfileOwner(headers, isComp); 306 } else { 307 initiateUIDeviceOwner(packageLabel, packageIcon, headers, customization); 308 } 309 } 310 initiateUIProfileOwner(@onNull String termsHeaders, boolean isComp)311 private void initiateUIProfileOwner(@NonNull String termsHeaders, boolean isComp) { 312 // set up the cancel button 313 Button cancelButton = (Button) findViewById(R.id.close_button); 314 cancelButton.setOnClickListener(v -> { 315 ProvisionLogger.logi("Close button (close_button) is clicked."); 316 PreProvisioningActivity.this.onBackPressed(); 317 }); 318 319 int messageId = isComp ? R.string.profile_owner_info_comp : R.string.profile_owner_info; 320 int messageWithTermsId = isComp ? R.string.profile_owner_info_with_terms_headers_comp 321 : R.string.profile_owner_info_with_terms_headers; 322 323 // set the short info text 324 TextView shortInfo = (TextView) findViewById(R.id.profile_owner_short_info); 325 shortInfo.setText(termsHeaders.isEmpty() 326 ? getString(messageId) 327 : getResources().getString(messageWithTermsId, termsHeaders)); 328 329 // set up show terms button 330 View viewTermsButton = findViewById(R.id.show_terms_button); 331 viewTermsButton.setOnClickListener(this::startViewTermsActivity); 332 mTouchTargetEnforcer.enforce(viewTermsButton, (View) viewTermsButton.getParent()); 333 334 // show the intro animation 335 mBenefitsAnimation = new BenefitsAnimation(this, 336 isComp ? SLIDE_CAPTIONS_COMP : SLIDE_CAPTIONS); 337 // TODO: move line below to be a part of BenefitsAnimation class 338 findViewById(R.id.animation_top_level_frame).setContentDescription(getString(isComp 339 ? R.string.comp_profile_benefits_description 340 : R.string.profile_benefits_description)); 341 } 342 initiateUIDeviceOwner(String packageName, Drawable packageIcon, @NonNull String termsHeaders, CustomizationParams customization)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 onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)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 startViewTermsActivity(@uppressWarnings"unused") View view)389 private void startViewTermsActivity(@SuppressWarnings("unused") View view) { 390 startActivity(createViewTermsIntent()); 391 } 392 createViewTermsIntent()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) assembleDOTermsMessage(@onNull String termsHeaders, String orgName)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 setDpcIconAndLabel(@onNull String appName, Drawable packageIcon, String orgName)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 showDeleteManagedProfileDialog(ComponentName mdmPackageName, String domainName, int userId)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 onBackPressed()450 public void onBackPressed() { 451 mController.logPreProvisioningCancelled(); 452 super.onBackPressed(); 453 } 454 455 @Override onResume()456 protected void onResume() { 457 super.onResume(); 458 if (mBenefitsAnimation != null) { 459 mBenefitsAnimation.start(); 460 } 461 } 462 463 @Override onPause()464 protected void onPause() { 465 super.onPause(); 466 if (mBenefitsAnimation != null) { 467 mBenefitsAnimation.stop(); 468 } 469 } 470 createImmutableList(int... values)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 */ getInstance(PreProvisioningActivity activity)489 PreProvisioningController getInstance(PreProvisioningActivity activity); 490 } 491 }