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 }