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}