Home | History | Annotate | Download | only in preprovisioning
      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 }