1 /*
2  * Copyright 2019, 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 package com.android.managedprovisioning.preprovisioning.consent;
17 
18 import static com.android.internal.util.Preconditions.checkNotNull;
19 import static java.util.Collections.emptyList;
20 import static java.util.Collections.unmodifiableList;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.Activity;
25 import android.content.Intent;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapFactory;
28 import android.graphics.drawable.BitmapDrawable;
29 import android.graphics.drawable.Drawable;
30 import android.text.Spannable;
31 import android.text.SpannableString;
32 import android.text.Spanned;
33 import android.text.TextUtils;
34 import android.text.method.LinkMovementMethod;
35 import android.text.style.ClickableSpan;
36 import android.view.View;
37 import android.widget.Button;
38 import android.widget.ImageView;
39 import android.widget.TextView;
40 import com.android.managedprovisioning.R;
41 import com.android.managedprovisioning.common.AccessibilityContextMenuMaker;
42 import com.android.managedprovisioning.common.ClickableSpanFactory;
43 import com.android.managedprovisioning.common.ProvisionLogger;
44 import com.android.managedprovisioning.common.StringConcatenator;
45 import com.android.managedprovisioning.common.TouchTargetEnforcer;
46 import com.android.managedprovisioning.common.Utils;
47 import com.android.managedprovisioning.model.CustomizationParams;
48 import com.android.managedprovisioning.preprovisioning.PreProvisioningController.UiParams;
49 import com.android.managedprovisioning.preprovisioning.anim.BenefitsAnimation;
50 import com.google.android.setupdesign.GlifLayout;
51 import java.util.ArrayList;
52 import java.util.List;
53 
54 /**
55  * Implements functionality for the legacy consent screen.
56  * <p>Android Auto still uses the old UI, so we need to ensure compatibility with it.
57  */
58 class LegacyConsentUiHelper implements ConsentUiHelper {
59     private static final List<Integer> SLIDE_CAPTIONS = createImmutableList(
60         R.string.info_anim_title_0,
61         R.string.info_anim_title_1,
62         R.string.info_anim_title_2);
63     private static final List<Integer> SLIDE_CAPTIONS_COMP = createImmutableList(
64         R.string.info_anim_title_0,
65         R.string.one_place_for_work_apps,
66         R.string.info_anim_title_2);
67 
68     private final Activity mActivity;
69     private final ClickableSpanFactory mClickableSpanFactory;
70     private final TouchTargetEnforcer mTouchTargetEnforcer;
71     private final ConsentUiHelperCallback mCallback;
72     private final Utils mUtils;
73     private @Nullable BenefitsAnimation mBenefitsAnimation;
74     private final AccessibilityContextMenuMaker mContextMenuMaker;
75 
LegacyConsentUiHelper(Activity activity, AccessibilityContextMenuMaker contextMenuMaker, ConsentUiHelperCallback callback, Utils utils)76     LegacyConsentUiHelper(Activity activity, AccessibilityContextMenuMaker contextMenuMaker,
77             ConsentUiHelperCallback callback, Utils utils) {
78         mActivity = checkNotNull(activity);
79         mContextMenuMaker = contextMenuMaker;
80         mCallback = callback;
81         mTouchTargetEnforcer =
82             new TouchTargetEnforcer(activity.getResources().getDisplayMetrics().density);
83         mClickableSpanFactory = new ClickableSpanFactory(mActivity.getColor(R.color.blue_text));
84         mUtils = utils;
85     }
86 
87     @Override
onStart()88     public void onStart() {
89         if (mBenefitsAnimation != null) {
90             mBenefitsAnimation.start();
91         }
92     }
93 
94     @Override
onStop()95     public void onStop() {
96         if (mBenefitsAnimation != null) {
97             mBenefitsAnimation.stop();
98         }
99     }
100 
setDpcIconAndLabel(@onNull String appName, Drawable packageIcon, String orgName)101     private void setDpcIconAndLabel(@NonNull String appName, Drawable packageIcon, String orgName) {
102         if (packageIcon == null || TextUtils.isEmpty(appName)) {
103             return;
104         }
105 
106         // make a container with all parts of DPC app description visible
107         mActivity.findViewById(R.id.intro_device_owner_app_info_container)
108             .setVisibility(View.VISIBLE);
109 
110         if (TextUtils.isEmpty(orgName)) {
111             orgName = mActivity.getString(R.string.your_organization_beginning);
112         }
113         String message = mActivity.getString(R.string.your_org_app_used, orgName);
114         TextView appInfoText = mActivity.findViewById(R.id.device_owner_app_info_text);
115         appInfoText.setText(message);
116 
117         ImageView imageView = mActivity.findViewById(R.id.device_manager_icon_view);
118         imageView.setImageDrawable(packageIcon);
119         imageView.setContentDescription(
120             mActivity.getResources().getString(R.string.mdm_icon_label, appName));
121 
122         TextView deviceManagerName = mActivity.findViewById(R.id.device_manager_name);
123         deviceManagerName.setText(appName);
124     }
125 
126     // TODO: refactor complex localized string assembly to an abstraction http://b/34288292
127     // there is a bit of copy-paste, and some details easy to forget (e.g. setMovementMethod)
assembleDOTermsMessage( @onNull String termsHeaders, String orgName, Intent showTermsIntent)128     private Spannable assembleDOTermsMessage(
129             @NonNull String termsHeaders, String orgName, Intent showTermsIntent) {
130         String linkText = mActivity.getString(R.string.view_terms);
131 
132         if (TextUtils.isEmpty(orgName)) {
133             orgName = mActivity.getString(R.string.your_organization_middle);
134         }
135         String messageText = termsHeaders.isEmpty()
136             ? mActivity.getString(R.string.device_owner_info, orgName, linkText)
137             : mActivity
138                 .getString(R.string.device_owner_info_with_terms_headers, orgName, termsHeaders,
139                     linkText);
140 
141         Spannable result = new SpannableString(messageText);
142         int start = messageText.indexOf(linkText);
143 
144         ClickableSpan span = mClickableSpanFactory.create(showTermsIntent);
145         result.setSpan(span, start, start + linkText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
146         return result;
147     }
148 
createImmutableList(int... values)149     private static List<Integer> createImmutableList(int... values) {
150         if (values == null || values.length == 0) {
151             return emptyList();
152         }
153         List<Integer> result = new ArrayList<>(values.length);
154         for (int value : values) {
155             result.add(value);
156         }
157         return unmodifiableList(result);
158     }
159 
160     @Override
initiateUi(UiParams uiParams)161     public void initiateUi(UiParams uiParams) {
162         int layoutResId = 0;
163         int titleResId = 0;
164         int headerResId = 0;
165         String packageLabel = null;
166         Drawable packageIcon = null;
167         if (mUtils.isProfileOwnerAction(uiParams.provisioningAction)) {
168             titleResId = R.string.setup_profile;
169             headerResId = R.string.work_profile_provisioning_accept_header;
170             layoutResId = R.layout.intro_profile_owner;
171         } else if (mUtils.isDeviceOwnerAction(uiParams.provisioningAction)) {
172             titleResId = R.string.setup_device;
173             headerResId = R.string.fully_managed_device_provisioning_accept_header;
174             layoutResId = R.layout.intro_device_owner;
175             packageLabel = uiParams.packageInfo != null
176                 ? uiParams.packageInfo.appLabel
177                 : uiParams.deviceAdminLabel != null
178                         ? uiParams.deviceAdminLabel : uiParams.packageName;
179             packageIcon = uiParams.packageInfo != null ? uiParams.packageInfo.packageIcon
180                 : getDeviceAdminIconDrawable(uiParams.deviceAdminIconFilePath);
181         }
182 
183         final CustomizationParams customization = uiParams.customization;
184         mCallback.initializeLayoutParams(
185                 layoutResId,
186                 headerResId,
187                 customization);
188 
189         setupAcceptAndContinueButton(uiParams.isSilentProvisioning);
190 
191         mActivity.setTitle(titleResId);
192 
193         final String headers =
194             new StringConcatenator(mActivity.getResources()).join(uiParams.disclaimerHeadings);
195         if (mUtils.isProfileOwnerAction(uiParams.provisioningAction)) {
196             initiateUIProfileOwner(headers, uiParams.isDeviceManaged, customization,
197                 uiParams.viewTermsIntent);
198         } else if (mUtils.isDeviceOwnerAction(uiParams.provisioningAction)){
199             initiateUIDeviceOwner(packageLabel, packageIcon, headers, customization,
200                 uiParams.viewTermsIntent);
201         }
202     }
203 
setupAcceptAndContinueButton(boolean isSilentProvisioning)204     private void setupAcceptAndContinueButton(boolean isSilentProvisioning) {
205         final GlifLayout layout = mActivity.findViewById(R.id.setup_wizard_layout);
206         Utils.addAcceptAndContinueButton(layout, v -> onNextButtonClicked());
207         if (isSilentProvisioning) {
208             onNextButtonClicked();
209         }
210     }
211 
onNextButtonClicked()212     private void onNextButtonClicked() {
213         ProvisionLogger.logi("Next button (next_button) is clicked.");
214         mCallback.nextAfterUserConsent();
215     }
216 
initiateUIProfileOwner(String termsHeaders, boolean isComp, CustomizationParams customizationParams, Intent showTermsIntent)217     private void initiateUIProfileOwner(String termsHeaders, boolean isComp,
218         CustomizationParams customizationParams, Intent showTermsIntent) {
219         // set up the back button
220         Button backButton = mActivity.findViewById(R.id.back_button);
221         backButton.setOnClickListener(v -> {
222             ProvisionLogger.logi("Back button (back_button) is clicked.");
223             mActivity.onBackPressed();
224         });
225 
226         int messageId = isComp ? R.string.profile_owner_info_comp : R.string.profile_owner_info;
227         int messageWithTermsId = isComp ? R.string.profile_owner_info_with_terms_headers_comp
228             : R.string.profile_owner_info_with_terms_headers;
229 
230         // set the short info text
231         TextView shortInfo = mActivity.findViewById(R.id.profile_owner_short_info);
232         shortInfo.setText(termsHeaders.isEmpty()
233             ? mActivity.getString(messageId)
234             : mActivity.getResources().getString(messageWithTermsId, termsHeaders));
235 
236         // set up show terms button
237         View viewTermsButton = mActivity.findViewById(R.id.show_terms_button);
238         viewTermsButton.setOnClickListener(v -> mActivity.startActivity(showTermsIntent));
239         mTouchTargetEnforcer.enforce(viewTermsButton, (View) viewTermsButton.getParent());
240 
241         // show the intro animation
242         mBenefitsAnimation = new BenefitsAnimation(
243             mActivity,
244             isComp
245                 ? SLIDE_CAPTIONS_COMP
246                 : SLIDE_CAPTIONS,
247             isComp
248                 ? R.string.comp_profile_benefits_description
249                 : R.string.profile_benefits_description,
250             customizationParams);
251     }
252 
initiateUIDeviceOwner( String packageName, Drawable packageIcon, String termsHeaders, CustomizationParams customization, Intent showTermsIntent)253     private void initiateUIDeviceOwner(
254             String packageName, Drawable packageIcon, String termsHeaders,
255         CustomizationParams customization, Intent showTermsIntent) {
256         // set up the cancel button if it exists
257         View cancelView = mActivity.findViewById(R.id.close_button);
258         if (cancelView != null) {
259             cancelView.setOnClickListener(v -> {
260                 ProvisionLogger.logi("Close button (close_button) is clicked.");
261                 mActivity.onBackPressed();
262             });
263         }
264 
265         // short terms info text with clickable 'view terms' link
266         TextView shortInfoText = mActivity.findViewById(R.id.device_owner_terms_info);
267         shortInfoText.setText(assembleDOTermsMessage(
268             termsHeaders, customization.orgName, showTermsIntent));
269         shortInfoText.setMovementMethod(LinkMovementMethod.getInstance()); // make clicks work
270         mContextMenuMaker.registerWithActivity(shortInfoText);
271 
272         handleSupportUrl(customization);
273 
274         // set up DPC icon and label
275         setDpcIconAndLabel(packageName, packageIcon, customization.orgName);
276     }
277 
handleSupportUrl(CustomizationParams customization)278     private void handleSupportUrl(CustomizationParams customization) {
279         final TextView info = mActivity.findViewById(R.id.device_owner_provider_info);
280         final String deviceProvider = mActivity.getString(R.string.organization_admin);
281         final String contactDeviceProvider =
282                 mActivity.getString(R.string.contact_device_provider, deviceProvider);
283         mUtils.handleSupportUrl(mActivity, customization, mClickableSpanFactory,
284                 mContextMenuMaker, info, deviceProvider, contactDeviceProvider);
285     }
286 
getDeviceAdminIconDrawable(@ullable String deviceAdminIconFilePath)287     private Drawable getDeviceAdminIconDrawable(@Nullable String deviceAdminIconFilePath) {
288         if (deviceAdminIconFilePath == null) {
289             return null;
290         }
291 
292         Bitmap bitmap = BitmapFactory.decodeFile(deviceAdminIconFilePath);
293         if (bitmap == null) {
294             return null;
295         }
296         return new BitmapDrawable(mActivity.getResources(), bitmap);
297     }
298 }
299