blob: 3625aeeccbc5511b0ff898aa6588d2f25dd5b8bb [file] [log] [blame]
/*
* Copyright 2016, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.managedprovisioning.preprovisioning;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import android.annotation.NonNull;
import android.app.Activity;
import android.app.DialogFragment;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.managedprovisioning.R;
import com.android.managedprovisioning.common.AccessibilityContextMenuMaker;
import com.android.managedprovisioning.common.ClickableSpanFactory;
import com.android.managedprovisioning.common.LogoUtils;
import com.android.managedprovisioning.common.ProvisionLogger;
import com.android.managedprovisioning.common.SetupGlifLayoutActivity;
import com.android.managedprovisioning.common.SimpleDialog;
import com.android.managedprovisioning.common.StringConcatenator;
import com.android.managedprovisioning.common.TouchTargetEnforcer;
import com.android.managedprovisioning.model.CustomizationParams;
import com.android.managedprovisioning.model.ProvisioningParams;
import com.android.managedprovisioning.preprovisioning.anim.BenefitsAnimation;
import com.android.managedprovisioning.preprovisioning.anim.ColorMatcher;
import com.android.managedprovisioning.preprovisioning.anim.SwiperThemeMatcher;
import com.android.managedprovisioning.preprovisioning.terms.TermsActivity;
import com.android.managedprovisioning.provisioning.ProvisioningActivity;
import java.util.ArrayList;
import java.util.List;
public class PreProvisioningActivity extends SetupGlifLayoutActivity implements
SimpleDialog.SimpleDialogListener, PreProvisioningController.Ui {
private static final List<Integer> SLIDE_CAPTIONS = createImmutableList(
R.string.info_anim_title_0,
R.string.info_anim_title_1,
R.string.info_anim_title_2);
private static final List<Integer> SLIDE_CAPTIONS_COMP = createImmutableList(
R.string.info_anim_title_0,
R.string.one_place_for_work_apps,
R.string.info_anim_title_2);
private static final int ENCRYPT_DEVICE_REQUEST_CODE = 1;
@VisibleForTesting
protected static final int PROVISIONING_REQUEST_CODE = 2;
private static final int WIFI_REQUEST_CODE = 3;
private static final int CHANGE_LAUNCHER_REQUEST_CODE = 4;
// Note: must match the constant defined in HomeSettings
private static final String EXTRA_SUPPORT_MANAGED_PROFILES = "support_managed_profiles";
private static final String SAVED_PROVISIONING_PARAMS = "saved_provisioning_params";
private static final String ERROR_AND_CLOSE_DIALOG = "PreProvErrorAndCloseDialog";
private static final String BACK_PRESSED_DIALOG = "PreProvBackPressedDialog";
private static final String CANCELLED_CONSENT_DIALOG = "PreProvCancelledConsentDialog";
private static final String LAUNCHER_INVALID_DIALOG = "PreProvCurrentLauncherInvalidDialog";
private static final String DELETE_MANAGED_PROFILE_DIALOG = "PreProvDeleteManagedProfileDialog";
private PreProvisioningController mController;
private ControllerProvider mControllerProvider;
private final AccessibilityContextMenuMaker mContextMenuMaker;
private BenefitsAnimation mBenefitsAnimation;
private ClickableSpanFactory mClickableSpanFactory;
private TouchTargetEnforcer mTouchTargetEnforcer;
public PreProvisioningActivity() {
this(activity -> new PreProvisioningController(activity, activity), null);
}
@VisibleForTesting
public PreProvisioningActivity(ControllerProvider controllerProvider,
AccessibilityContextMenuMaker contextMenuMaker) {
mControllerProvider = controllerProvider;
mContextMenuMaker =
contextMenuMaker != null ? contextMenuMaker : new AccessibilityContextMenuMaker(
this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mClickableSpanFactory = new ClickableSpanFactory(getColor(R.color.blue));
mTouchTargetEnforcer = new TouchTargetEnforcer(getResources().getDisplayMetrics().density);
mController = mControllerProvider.getInstance(this);
ProvisioningParams params = savedInstanceState == null ? null
: savedInstanceState.getParcelable(SAVED_PROVISIONING_PARAMS);
mController.initiateProvisioning(getIntent(), params, getCallingPackage());
}
@Override
public void finish() {
// The user has backed out of provisioning, so we perform the necessary clean up steps.
LogoUtils.cleanUp(this);
ProvisioningParams params = mController.getParams();
if (params != null) {
params.cleanUp();
}
EncryptionController.getInstance(this).cancelEncryptionReminder();
super.finish();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(SAVED_PROVISIONING_PARAMS, mController.getParams());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case ENCRYPT_DEVICE_REQUEST_CODE:
if (resultCode == RESULT_CANCELED) {
ProvisionLogger.loge("User canceled device encryption.");
}
break;
case PROVISIONING_REQUEST_CODE:
setResult(resultCode);
finish();
break;
case CHANGE_LAUNCHER_REQUEST_CODE:
mController.continueProvisioningAfterUserConsent();
break;
case WIFI_REQUEST_CODE:
if (resultCode == RESULT_CANCELED) {
ProvisionLogger.loge("User canceled wifi picking.");
} else if (resultCode == RESULT_OK) {
ProvisionLogger.logd("Wifi request result is OK");
}
mController.initiateProvisioning(getIntent(), null /* cached params */,
getCallingPackage());
break;
default:
ProvisionLogger.logw("Unknown result code :" + resultCode);
break;
}
}
@Override
public void showErrorAndClose(Integer titleId, int messageId, String logText) {
ProvisionLogger.loge(logText);
SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder()
.setTitle(titleId)
.setMessage(messageId)
.setCancelable(false)
.setPositiveButtonMessage(R.string.device_owner_error_ok);
showDialog(dialogBuilder, ERROR_AND_CLOSE_DIALOG);
}
@Override
public void onNegativeButtonClick(DialogFragment dialog) {
switch (dialog.getTag()) {
case CANCELLED_CONSENT_DIALOG:
case BACK_PRESSED_DIALOG:
// user chose to continue. Do nothing
break;
case LAUNCHER_INVALID_DIALOG:
dialog.dismiss();
break;
case DELETE_MANAGED_PROFILE_DIALOG:
setResult(Activity.RESULT_CANCELED);
finish();
break;
default:
SimpleDialog.throwButtonClickHandlerNotImplemented(dialog);
}
}
@Override
public void onPositiveButtonClick(DialogFragment dialog) {
switch (dialog.getTag()) {
case ERROR_AND_CLOSE_DIALOG:
case BACK_PRESSED_DIALOG:
// Close activity
setResult(Activity.RESULT_CANCELED);
// TODO: Move logging to close button, if we finish provisioning there.
mController.logPreProvisioningCancelled();
finish();
break;
case CANCELLED_CONSENT_DIALOG:
mUtils.sendFactoryResetBroadcast(this, "Device owner setup cancelled");
break;
case LAUNCHER_INVALID_DIALOG:
requestLauncherPick();
break;
case DELETE_MANAGED_PROFILE_DIALOG:
DeleteManagedProfileDialog d = (DeleteManagedProfileDialog) dialog;
mController.removeUser(d.getUserId());
// TODO: refactor as evil - logic should be less spread out
// Check if we are in the middle of silent provisioning and were got blocked by an
// existing user profile. If so, we can now resume.
mController.checkResumeSilentProvisioning();
break;
default:
SimpleDialog.throwButtonClickHandlerNotImplemented(dialog);
}
}
@Override
public void requestEncryption(ProvisioningParams params) {
Intent encryptIntent = new Intent(this, EncryptDeviceActivity.class);
encryptIntent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params);
startActivityForResult(encryptIntent, ENCRYPT_DEVICE_REQUEST_CODE);
}
@Override
public void requestWifiPick() {
startActivityForResult(mUtils.getWifiPickIntent(), WIFI_REQUEST_CODE);
}
@Override
public void showCurrentLauncherInvalid() {
SimpleDialog.Builder dialogBuilder = new SimpleDialog.Builder()
.setCancelable(false)
.setTitle(R.string.change_device_launcher)
.setMessage(R.string.launcher_app_cant_be_used_by_work_profile)
.setNegativeButtonMessage(R.string.cancel_provisioning)
.setPositiveButtonMessage(R.string.pick_launcher);
showDialog(dialogBuilder, LAUNCHER_INVALID_DIALOG);
}
private void requestLauncherPick() {
Intent changeLauncherIntent = new Intent(Settings.ACTION_HOME_SETTINGS);
changeLauncherIntent.putExtra(EXTRA_SUPPORT_MANAGED_PROFILES, true);
startActivityForResult(changeLauncherIntent, CHANGE_LAUNCHER_REQUEST_CODE);
}
public void startProvisioning(int userId, ProvisioningParams params) {
Intent intent = new Intent(this, ProvisioningActivity.class);
intent.putExtra(ProvisioningParams.EXTRA_PROVISIONING_PARAMS, params);
startActivityForResultAsUser(intent, PROVISIONING_REQUEST_CODE, new UserHandle(userId));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}
@Override
public void initiateUi(int layoutId, int titleId, String packageLabel, Drawable packageIcon,
boolean isProfileOwnerProvisioning, boolean isComp, List<String> termsHeaders,
CustomizationParams customization) {
if (isProfileOwnerProvisioning) {
// setting a theme so that the animation swiper matches the mainColor
// needs to happen before {@link Activity#setContentView}
setTheme(new SwiperThemeMatcher(this,
new ColorMatcher()) // TODO: introduce DI framework
.findTheme(customization.swiperColor));
}
initializeLayoutParams(
layoutId,
isProfileOwnerProvisioning ? null : R.string.set_up_your_device,
false /* progress bar */,
customization.statusBarColor);
// set up the 'accept and continue' button
Button nextButton = (Button) findViewById(R.id.next_button);
nextButton.setOnClickListener(v -> {
ProvisionLogger.logi("Next button (next_button) is clicked.");
mController.continueProvisioningAfterUserConsent();
});
nextButton.setBackgroundTintList(ColorStateList.valueOf(customization.buttonColor));
if (mUtils.isBrightColor(customization.buttonColor)) {
nextButton.setTextColor(getColor(R.color.gray_button_text));
}
// set the activity title
setTitle(titleId);
// set up terms headers
String headers = new StringConcatenator(getResources()).join(termsHeaders);
// initiate UI for MP / DO
if (isProfileOwnerProvisioning) {
initiateUIProfileOwner(headers, isComp);
} else {
initiateUIDeviceOwner(packageLabel, packageIcon, headers, customization);
}
}
private void initiateUIProfileOwner(@NonNull String termsHeaders, boolean isComp) {
// set up the cancel button
Button cancelButton = (Button) findViewById(R.id.close_button);
cancelButton.setOnClickListener(v -> {
ProvisionLogger.logi("Close button (close_button) is clicked.");
PreProvisioningActivity.this.onBackPressed();
});
int messageId = isComp ? R.string.profile_owner_info_comp : R.string.profile_owner_info;
int messageWithTermsId = isComp ? R.string.profile_owner_info_with_terms_headers_comp
: R.string.profile_owner_info_with_terms_headers;
// set the short info text
TextView shortInfo = (TextView) findViewById(R.id.profile_owner_short_info);
shortInfo.setText(termsHeaders.isEmpty()
? getString(messageId)
: getResources().getString(messageWithTermsId, termsHeaders));
// set up show terms button
View viewTermsButton = findViewById(R.id.show_terms_button);
viewTermsButton.setOnClickListener(this::startViewTermsActivity);
mTouchTargetEnforcer.enforce(viewTermsButton, (View) viewTermsButton.getParent());
// show the intro animation
mBenefitsAnimation = new BenefitsAnimation(
this,
isComp
? SLIDE_CAPTIONS_COMP
: SLIDE_CAPTIONS,
isComp
? R.string.comp_profile_benefits_description
: R.string.profile_benefits_description);
}
private void initiateUIDeviceOwner(String packageName, Drawable packageIcon,
@NonNull String termsHeaders, CustomizationParams customization) {
// short terms info text with clickable 'view terms' link
TextView shortInfoText = (TextView) findViewById(R.id.device_owner_terms_info);
shortInfoText.setText(assembleDOTermsMessage(termsHeaders, customization.orgName));
shortInfoText.setMovementMethod(LinkMovementMethod.getInstance()); // make clicks work
mContextMenuMaker.registerWithActivity(shortInfoText);
// if you have any questions, contact your device's provider
//
// TODO: refactor complex localized string assembly to an abstraction http://b/34288292
// there is a bit of copy-paste, and some details easy to forget (e.g. setMovementMethod)
if (customization.supportUrl != null) {
TextView info = (TextView) findViewById(R.id.device_owner_provider_info);
info.setVisibility(View.VISIBLE);
String deviceProvider = getString(R.string.organization_admin);
String contactDeviceProvider = getString(R.string.contact_device_provider,
deviceProvider);
SpannableString spannableString = new SpannableString(contactDeviceProvider);
Intent intent = WebActivity.createIntent(this, customization.supportUrl,
customization.statusBarColor);
if (intent != null) {
ClickableSpan span = mClickableSpanFactory.create(intent);
int startIx = contactDeviceProvider.indexOf(deviceProvider);
int endIx = startIx + deviceProvider.length();
spannableString.setSpan(span, startIx, endIx, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
info.setMovementMethod(LinkMovementMethod.getInstance()); // make clicks work
}
info.setText(spannableString);
mContextMenuMaker.registerWithActivity(info);
}
// set up DPC icon and label
setDpcIconAndLabel(packageName, packageIcon, customization.orgName);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
if (v instanceof TextView) {
mContextMenuMaker.populateMenuContent(menu, (TextView) v);
}
}
private void startViewTermsActivity(@SuppressWarnings("unused") View view) {
startActivity(createViewTermsIntent());
}
private Intent createViewTermsIntent() {
return new Intent(this, TermsActivity.class).putExtra(
ProvisioningParams.EXTRA_PROVISIONING_PARAMS, mController.getParams());
}
// TODO: refactor complex localized string assembly to an abstraction http://b/34288292
// there is a bit of copy-paste, and some details easy to forget (e.g. setMovementMethod)
private Spannable assembleDOTermsMessage(@NonNull String termsHeaders, String orgName) {
String linkText = getString(R.string.view_terms);
if (TextUtils.isEmpty(orgName)) {
orgName = getString(R.string.your_organization_middle);
}
String messageText = termsHeaders.isEmpty()
? getString(R.string.device_owner_info, orgName, linkText)
: getString(R.string.device_owner_info_with_terms_headers, orgName, termsHeaders,
linkText);
Spannable result = new SpannableString(messageText);
int start = messageText.indexOf(linkText);
ClickableSpan span = mClickableSpanFactory.create(createViewTermsIntent());
result.setSpan(span, start, start + linkText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return result;
}
private void setDpcIconAndLabel(@NonNull String appName, Drawable packageIcon, String orgName) {
if (packageIcon == null || TextUtils.isEmpty(appName)) {
return;
}
// make a container with all parts of DPC app description visible
findViewById(R.id.intro_device_owner_app_info_container).setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(orgName)) {
orgName = getString(R.string.your_organization_beginning);
}
String message = getString(R.string.your_org_app_used, orgName);
TextView appInfoText = (TextView) findViewById(R.id.device_owner_app_info_text);
appInfoText.setText(message);
ImageView imageView = (ImageView) findViewById(R.id.device_manager_icon_view);
imageView.setImageDrawable(packageIcon);
imageView.setContentDescription(getResources().getString(R.string.mdm_icon_label, appName));
TextView deviceManagerName = (TextView) findViewById(R.id.device_manager_name);
deviceManagerName.setText(appName);
}
@Override
public void showDeleteManagedProfileDialog(ComponentName mdmPackageName, String domainName,
int userId) {
showDialog(() -> DeleteManagedProfileDialog.newInstance(userId,
mdmPackageName, domainName), DELETE_MANAGED_PROFILE_DIALOG);
}
@Override
public void onBackPressed() {
mController.logPreProvisioningCancelled();
super.onBackPressed();
}
@Override
protected void onResume() {
super.onResume();
if (mBenefitsAnimation != null) {
mBenefitsAnimation.start();
}
}
@Override
protected void onPause() {
super.onPause();
if (mBenefitsAnimation != null) {
mBenefitsAnimation.stop();
}
}
private static List<Integer> createImmutableList(int... values) {
if (values == null || values.length == 0) {
return emptyList();
}
List<Integer> result = new ArrayList<>(values.length);
for (int value : values) {
result.add(value);
}
return unmodifiableList(result);
}
/**
* Constructs {@link PreProvisioningController} for a given {@link PreProvisioningActivity}
*/
interface ControllerProvider {
/**
* Constructs {@link PreProvisioningController} for a given {@link PreProvisioningActivity}
*/
PreProvisioningController getInstance(PreProvisioningActivity activity);
}
}