• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.support.annotation.VisibleForTesting;
33 import android.text.Spannable;
34 import android.text.SpannableString;
35 import android.text.Spanned;
36 import android.text.TextUtils;
37 import android.text.method.LinkMovementMethod;
38 import android.text.style.ClickableSpan;
39 import android.view.ContextMenu;
40 import android.view.ContextMenu.ContextMenuInfo;
41 import android.view.View;
42 import android.widget.Button;
43 import android.widget.ImageView;
44 import android.widget.TextView;
45 
46 import com.android.managedprovisioning.R;
47 import com.android.managedprovisioning.common.AccessibilityContextMenuMaker;
48 import com.android.managedprovisioning.common.ClickableSpanFactory;
49 import com.android.managedprovisioning.common.LogoUtils;
50 import com.android.managedprovisioning.common.ProvisionLogger;
51 import com.android.managedprovisioning.common.SetupGlifLayoutActivity;
52 import com.android.managedprovisioning.common.SimpleDialog;
53 import com.android.managedprovisioning.common.StringConcatenator;
54 import com.android.managedprovisioning.common.TouchTargetEnforcer;
55 import com.android.managedprovisioning.common.Utils;
56 import com.android.managedprovisioning.model.CustomizationParams;
57 import com.android.managedprovisioning.model.ProvisioningParams;
58 import com.android.managedprovisioning.preprovisioning.anim.BenefitsAnimation;
59 import com.android.managedprovisioning.preprovisioning.anim.ColorMatcher;
60 import com.android.managedprovisioning.preprovisioning.anim.SwiperThemeMatcher;
61 import com.android.managedprovisioning.preprovisioning.terms.TermsActivity;
62 import com.android.managedprovisioning.provisioning.ProvisioningActivity;
63 
64 import java.util.ArrayList;
65 import java.util.List;
66 
67 public 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 
PreProvisioningActivity()101     public PreProvisioningActivity() {
102         this(activity -> new PreProvisioningController(activity, activity), null, new Utils());
103     }
104 
105     @VisibleForTesting
PreProvisioningActivity(ControllerProvider controllerProvider, AccessibilityContextMenuMaker contextMenuMaker, Utils utils)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
onCreate(Bundle savedInstanceState)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
finish()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
onSaveInstanceState(Bundle outState)140     protected void onSaveInstanceState(Bundle outState) {
141         super.onSaveInstanceState(outState);
142         outState.putParcelable(SAVED_PROVISIONING_PARAMS, mController.getParams());
143     }
144 
145     @Override
onActivityResult(int requestCode, int resultCode, Intent data)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
showErrorAndClose(Integer titleId, int messageId, String logText)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
onNegativeButtonClick(DialogFragment dialog)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
onPositiveButtonClick(DialogFragment dialog)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
requestEncryption(ProvisioningParams params)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
requestWifiPick()244     public void requestWifiPick() {
245         startActivityForResult(mUtils.getWifiPickIntent(), WIFI_REQUEST_CODE);
246     }
247 
248     @Override
showCurrentLauncherInvalid()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 
requestLauncherPick()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 
startProvisioning(int userId, ProvisioningParams params)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
initiateUi(int layoutId, int titleId, String packageLabel, Drawable packageIcon, boolean isProfileOwnerProvisioning, boolean isComp, List<String> termsHeaders, CustomizationParams customization)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 
initiateUIProfileOwner( @onNull String termsHeaders, boolean isComp, CustomizationParams customizationParams)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 
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 }