1 /*
2  * Copyright (C) 2020 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.settings.applications.specialaccess.interactacrossprofiles;
17 
18 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
19 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
20 import static android.provider.Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS;
21 
22 import android.Manifest;
23 import android.annotation.UserIdInt;
24 import android.app.ActionBar;
25 import android.app.AppOpsManager;
26 import android.app.admin.DevicePolicyEventLogger;
27 import android.app.admin.DevicePolicyManager;
28 import android.app.settings.SettingsEnums;
29 import android.content.Context;
30 import android.content.DialogInterface;
31 import android.content.Intent;
32 import android.content.PermissionChecker;
33 import android.content.pm.CrossProfileApps;
34 import android.content.pm.PackageInfo;
35 import android.content.pm.PackageManager;
36 import android.graphics.ColorMatrix;
37 import android.graphics.ColorMatrixColorFilter;
38 import android.graphics.drawable.Drawable;
39 import android.os.Bundle;
40 import android.os.UserHandle;
41 import android.os.UserManager;
42 import android.stats.devicepolicy.DevicePolicyEnums;
43 import android.util.IconDrawableFactory;
44 import android.view.View;
45 import android.widget.ImageView;
46 import android.widget.TextView;
47 
48 import androidx.appcompat.app.AlertDialog;
49 import androidx.preference.Preference;
50 
51 import com.android.settings.R;
52 import com.android.settings.applications.AppInfoBase;
53 import com.android.settings.applications.AppStoreUtil;
54 import com.android.settings.widget.CardPreference;
55 import com.android.settingslib.RestrictedLockUtils;
56 import com.android.settingslib.RestrictedSwitchPreference;
57 import com.android.settingslib.widget.LayoutPreference;
58 
59 public class InteractAcrossProfilesDetails extends AppInfoBase
60         implements Preference.OnPreferenceClickListener {
61 
62     private static final String INTERACT_ACROSS_PROFILES_SETTINGS_SWITCH =
63             "interact_across_profiles_settings_switch";
64     private static final String INTERACT_ACROSS_PROFILES_HEADER = "interact_across_profiles_header";
65     public static final String INSTALL_APP_BANNER_KEY = "install_app_banner";
66     public static final String INTERACT_ACROSS_PROFILE_EXTRA_SUMMARY_KEY =
67             "interact_across_profiles_extra_summary";
68     public static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
69     public static final String INTENT_KEY = "intent";
70 
71     private Context mContext;
72     private CrossProfileApps mCrossProfileApps;
73     private UserManager mUserManager;
74     private RestrictedSwitchPreference mSwitchPref;
75     private LayoutPreference mHeader;
76     private CardPreference mInstallBanner;
77     private PackageManager mPackageManager;
78     private UserHandle mPersonalProfile;
79     private UserHandle mWorkProfile;
80     private boolean mInstalledInPersonal;
81     private boolean mInstalledInWork;
82     private String mAppLabel;
83     private Intent mInstallAppIntent;
84     private boolean mIsPageLaunchedByApp;
85 
86     @Override
onCreate(Bundle savedInstanceState)87     public void onCreate(Bundle savedInstanceState) {
88         super.onCreate(savedInstanceState);
89 
90         mContext = getContext();
91         mCrossProfileApps = mContext.getSystemService(CrossProfileApps.class);
92         mUserManager = mContext.getSystemService(UserManager.class);
93         mPackageManager = mContext.getPackageManager();
94 
95         mWorkProfile = InteractAcrossProfilesSettings.getWorkProfile(mUserManager);
96         mPersonalProfile = mUserManager.getProfileParent(mWorkProfile);
97         mInstalledInWork = isPackageInstalled(mPackageName, mWorkProfile.getIdentifier());
98         mInstalledInPersonal = isPackageInstalled(mPackageName, mPersonalProfile.getIdentifier());
99 
100         mAppLabel = mPackageInfo.applicationInfo.loadLabel(mPackageManager).toString();
101         mInstallAppIntent = AppStoreUtil.getAppStoreLink(mContext, mPackageName);
102 
103         addPreferencesFromResource(R.xml.interact_across_profiles_permissions_details);
104         mSwitchPref = findPreference(INTERACT_ACROSS_PROFILES_SETTINGS_SWITCH);
105         mSwitchPref.setOnPreferenceClickListener(this);
106 
107         mHeader = findPreference(INTERACT_ACROSS_PROFILES_HEADER);
108 
109         mInstallBanner = findPreference(INSTALL_APP_BANNER_KEY);
110         mInstallBanner.setOnPreferenceClickListener(this);
111 
112         mIsPageLaunchedByApp = launchedByApp();
113 
114         // refreshUi checks that the user can still configure the appOp, return to the
115         // previous page if it can't.
116         if (!refreshUi()) {
117             setIntentAndFinish(true/* appChanged */);
118         }
119         addAppTitleAndIcons(mPersonalProfile, mWorkProfile);
120         styleActionBar();
121         maybeShowExtraSummary();
122         logPageLaunchMetrics();
123     }
124 
maybeShowExtraSummary()125     private void maybeShowExtraSummary() {
126         Preference extraSummary = findPreference(INTERACT_ACROSS_PROFILE_EXTRA_SUMMARY_KEY);
127         if (extraSummary == null) {
128             return;
129         }
130         extraSummary.setVisible(mIsPageLaunchedByApp);
131     }
132 
logPageLaunchMetrics()133     private void logPageLaunchMetrics() {
134         if (!mCrossProfileApps.canConfigureInteractAcrossProfiles(mPackageName)) {
135             logNonConfigurableAppMetrics();
136         }
137         if (mIsPageLaunchedByApp) {
138             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_LAUNCHED_FROM_APP);
139         } else {
140             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_LAUNCHED_FROM_SETTINGS);
141         }
142     }
143 
logNonConfigurableAppMetrics()144     private void logNonConfigurableAppMetrics() {
145         if (!isCrossProfilePackageWhitelisted(mPackageName)) {
146             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_ADMIN_RESTRICTED);
147             return;
148         }
149         if (mInstallBanner == null) {
150             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_MISSING_INSTALL_BANNER_INTENT);
151         }
152         if (!mInstalledInPersonal) {
153             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_MISSING_PERSONAL_APP);
154             return;
155         }
156         if (!mInstalledInWork) {
157             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_MISSING_WORK_APP);
158         }
159     }
160 
logEvent(int eventId)161     private void logEvent(int eventId) {
162         DevicePolicyEventLogger.createEvent(eventId)
163                 .setStrings(mPackageName)
164                 .setInt(UserHandle.myUserId())
165                 .setAdmin(RestrictedLockUtils.getProfileOrDeviceOwner(
166                         mContext, mWorkProfile).component)
167                 .write();
168     }
169 
addAppTitleAndIcons(UserHandle personalProfile, UserHandle workProfile)170     private void addAppTitleAndIcons(UserHandle personalProfile, UserHandle workProfile) {
171         final TextView title = mHeader.findViewById(R.id.entity_header_title);
172         if (title != null) {
173             final String appLabel = mPackageInfo.applicationInfo.loadLabel(
174                     mPackageManager).toString();
175             title.setText(appLabel);
176         }
177 
178         final ImageView personalIconView = mHeader.findViewById(R.id.entity_header_icon_personal);
179         if (personalIconView != null) {
180             Drawable icon = IconDrawableFactory.newInstance(mContext)
181                     .getBadgedIcon(mPackageInfo.applicationInfo, personalProfile.getIdentifier())
182                     .mutate();
183             if (!mInstalledInPersonal) {
184                 icon.setColorFilter(createSuspendedColorMatrix());
185             }
186             personalIconView.setImageDrawable(icon);
187         }
188 
189         final ImageView workIconView = mHeader.findViewById(R.id.entity_header_icon_work);
190         if (workIconView != null) {
191             Drawable icon = IconDrawableFactory.newInstance(mContext)
192                     .getBadgedIcon(mPackageInfo.applicationInfo, workProfile.getIdentifier())
193                     .mutate();
194             if (!mInstalledInWork) {
195                 icon.setColorFilter(createSuspendedColorMatrix());
196             }
197             workIconView.setImageDrawable(icon);
198         }
199     }
200 
styleActionBar()201     private void styleActionBar() {
202         final ActionBar actionBar = getActivity().getActionBar();
203         if (actionBar != null) {
204             actionBar.setElevation(0);
205         }
206     }
207 
createSuspendedColorMatrix()208     private ColorMatrixColorFilter createSuspendedColorMatrix() {
209         int grayValue = 127;
210         float scale = 0.5f; // half bright
211 
212         ColorMatrix tempBrightnessMatrix = new ColorMatrix();
213         float[] mat = tempBrightnessMatrix.getArray();
214         mat[0] = scale;
215         mat[6] = scale;
216         mat[12] = scale;
217         mat[4] = grayValue;
218         mat[9] = grayValue;
219         mat[14] = grayValue;
220 
221         ColorMatrix matrix = new ColorMatrix();
222         matrix.setSaturation(0.0f);
223         matrix.preConcat(tempBrightnessMatrix);
224         return new ColorMatrixColorFilter(matrix);
225     }
226 
227     @Override
onPreferenceClick(Preference preference)228     public boolean onPreferenceClick(Preference preference) {
229         // refreshUi checks that the user can still configure the appOp, return to the
230         // previous page if it can't.
231         if (!refreshUi()) {
232             setIntentAndFinish(true/* appChanged */);
233         }
234         if (preference == mSwitchPref) {
235             handleSwitchPreferenceClick();
236             return true;
237         }
238         if (preference == mInstallBanner) {
239             handleInstallBannerClick();
240             return true;
241         }
242         return false;
243     }
244 
handleSwitchPreferenceClick()245     private void handleSwitchPreferenceClick() {
246         if (isInteractAcrossProfilesEnabled()) {
247             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_PERMISSION_REVOKED);
248             enableInteractAcrossProfiles(false);
249             refreshUi();
250         } else {
251             showConsentDialog();
252         }
253     }
254 
showConsentDialog()255     private void showConsentDialog() {
256         final View dialogView = getLayoutInflater().inflate(
257                 R.layout.interact_across_profiles_consent_dialog, null);
258 
259         final TextView dialogTitle = dialogView.findViewById(
260                 R.id.interact_across_profiles_consent_dialog_title);
261         dialogTitle.setText(
262                 getString(R.string.interact_across_profiles_consent_dialog_title, mAppLabel));
263 
264         final TextView appDataSummary = dialogView.findViewById(R.id.app_data_summary);
265         appDataSummary.setText(getString(
266                 R.string.interact_across_profiles_consent_dialog_app_data_summary, mAppLabel));
267 
268         final TextView permissionsSummary = dialogView.findViewById(R.id.permissions_summary);
269         permissionsSummary.setText(getString(
270                 R.string.interact_across_profiles_consent_dialog_permissions_summary, mAppLabel));
271 
272         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
273         builder.setView(dialogView)
274                 .setPositiveButton(R.string.allow, new DialogInterface.OnClickListener() {
275                     public void onClick(DialogInterface dialog, int which) {
276                         logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_USER_CONSENTED);
277                         enableInteractAcrossProfiles(true);
278                         refreshUi();
279                         if (mIsPageLaunchedByApp) {
280                             setIntentAndFinish(/* appChanged= */ true);
281                         }
282                     }
283                 })
284                 .setNegativeButton(R.string.deny, new DialogInterface.OnClickListener() {
285                     public void onClick(DialogInterface dialog, int which) {
286                         logEvent(
287                                 DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_USER_DECLINED_CONSENT);
288                         refreshUi();
289                     }
290                 })
291                 .create().show();
292     }
293 
isInteractAcrossProfilesEnabled()294     private boolean isInteractAcrossProfilesEnabled() {
295         return isInteractAcrossProfilesEnabled(mContext, mPackageName);
296     }
297 
isInteractAcrossProfilesEnabled( Context context, String packageName)298     static boolean isInteractAcrossProfilesEnabled(
299             Context context, String packageName) {
300         UserManager userManager = context.getSystemService(UserManager.class);
301         UserHandle workProfile = InteractAcrossProfilesSettings.getWorkProfile(userManager);
302         if (workProfile == null) {
303             return false;
304         }
305         UserHandle personalProfile = userManager.getProfileParent(workProfile);
306         return context.getSystemService(
307                 CrossProfileApps.class).canConfigureInteractAcrossProfiles(packageName)
308                 && isInteractAcrossProfilesEnabledInProfile(context, packageName, personalProfile)
309                 && isInteractAcrossProfilesEnabledInProfile(context, packageName, workProfile);
310 
311     }
312 
isInteractAcrossProfilesEnabledInProfile( Context context, String packageName, UserHandle userHandle)313     private static boolean isInteractAcrossProfilesEnabledInProfile(
314             Context context, String packageName, UserHandle userHandle) {
315         final PackageManager packageManager = context.getPackageManager();
316         final int uid;
317         try {
318             uid = packageManager.getApplicationInfoAsUser(
319                     packageName, /* flags= */0, userHandle).uid;
320         } catch (PackageManager.NameNotFoundException e) {
321             return false;
322         }
323         return PermissionChecker.PERMISSION_GRANTED
324                 == PermissionChecker.checkPermissionForPreflight(
325                 context,
326                 Manifest.permission.INTERACT_ACROSS_PROFILES,
327                 PermissionChecker.PID_UNKNOWN,
328                 uid,
329                 packageName);
330     }
331 
enableInteractAcrossProfiles(boolean newState)332     private void enableInteractAcrossProfiles(boolean newState) {
333         mCrossProfileApps.setInteractAcrossProfilesAppOp(
334                 mPackageName, newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
335     }
336 
handleInstallBannerClick()337     private void handleInstallBannerClick() {
338         if (mInstallAppIntent == null) {
339             logEvent(
340                     DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_INSTALL_BANNER_NO_INTENT_CLICKED);
341             return;
342         }
343         if (!mInstalledInWork) {
344             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_INSTALL_BANNER_CLICKED);
345             mContext.startActivityAsUser(mInstallAppIntent, mWorkProfile);
346             return;
347         }
348         if (!mInstalledInPersonal) {
349             logEvent(DevicePolicyEnums.CROSS_PROFILE_SETTINGS_PAGE_INSTALL_BANNER_CLICKED);
350             mContext.startActivityAsUser(mInstallAppIntent, mPersonalProfile);
351         }
352     }
353 
354     /**
355      * @return the summary for the current state of whether the app associated with the given
356      * {@code packageName} is allowed to interact across profiles.
357      */
getPreferenceSummary( Context context, String packageName)358     public static CharSequence getPreferenceSummary(
359             Context context, String packageName) {
360         return context.getString(isInteractAcrossProfilesEnabled(context, packageName)
361                 ? R.string.interact_across_profiles_summary_allowed
362                 : R.string.interact_across_profiles_summary_not_allowed);
363     }
364 
365     @Override
refreshUi()366     protected boolean refreshUi() {
367         if (mPackageInfo == null || mPackageInfo.applicationInfo == null) {
368             return false;
369         }
370         if (!mCrossProfileApps.canUserAttemptToConfigureInteractAcrossProfiles(mPackageName)) {
371             // Invalid app entry. Should not allow changing permission
372             mSwitchPref.setEnabled(false);
373             return false;
374         }
375         if (!mCrossProfileApps.canConfigureInteractAcrossProfiles(mPackageName)) {
376             return refreshUiForNonConfigurableApps();
377         }
378         refreshUiForConfigurableApps();
379         return true;
380     }
381 
refreshUiForNonConfigurableApps()382     private boolean refreshUiForNonConfigurableApps() {
383         mSwitchPref.setChecked(false);
384         mSwitchPref.setTitle(R.string.interact_across_profiles_switch_disabled);
385         if (!isCrossProfilePackageWhitelisted(mPackageName)) {
386             mInstallBanner.setVisible(false);
387             mSwitchPref.setDisabledByAdmin(RestrictedLockUtils.getProfileOrDeviceOwner(
388                     mContext, mWorkProfile));
389             return true;
390         }
391         mSwitchPref.setEnabled(false);
392         if (!mInstalledInPersonal && !mInstalledInWork) {
393             return false;
394         }
395         if (!mInstalledInPersonal) {
396             mInstallBanner.setTitle(getString(
397                     R.string.interact_across_profiles_install_personal_app_title,
398                     mAppLabel));
399             if (mInstallAppIntent != null) {
400                 mInstallBanner.setSummary(
401                         R.string.interact_across_profiles_install_app_summary);
402             }
403             mInstallBanner.setVisible(true);
404             return true;
405         }
406         if (!mInstalledInWork) {
407             mInstallBanner.setTitle(getString(
408                     R.string.interact_across_profiles_install_work_app_title,
409                     mAppLabel));
410             if (mInstallAppIntent != null) {
411                 mInstallBanner.setSummary(
412                         R.string.interact_across_profiles_install_app_summary);
413             }
414             mInstallBanner.setVisible(true);
415             return true;
416         }
417         return false;
418     }
419 
isCrossProfilePackageWhitelisted(String packageName)420     private boolean isCrossProfilePackageWhitelisted(String packageName) {
421         return mContext.getSystemService(DevicePolicyManager.class)
422                 .getAllCrossProfilePackages().contains(packageName);
423     }
424 
isPackageInstalled(String packageName, @UserIdInt int userId)425     private boolean isPackageInstalled(String packageName, @UserIdInt int userId) {
426         final PackageInfo info;
427         try {
428             info = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */0)
429                     .getPackageManager().getPackageInfo(packageName,
430                             MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE);
431         } catch (PackageManager.NameNotFoundException e) {
432             return false;
433         }
434         return info != null;
435     }
436 
refreshUiForConfigurableApps()437     private void refreshUiForConfigurableApps() {
438         mInstallBanner.setVisible(false);
439         mSwitchPref.setEnabled(true);
440         if (isInteractAcrossProfilesEnabled()) {
441             enableSwitchPref();
442         } else {
443             disableSwitchPref();
444         }
445     }
446 
enableSwitchPref()447     private void enableSwitchPref() {
448         mSwitchPref.setChecked(true);
449         mSwitchPref.setTitle(R.string.interact_across_profiles_switch_enabled);
450         final ImageView horizontalArrowIcon = mHeader.findViewById(R.id.entity_header_swap_horiz);
451         if (horizontalArrowIcon != null) {
452             horizontalArrowIcon.setImageDrawable(
453                     mContext.getDrawable(R.drawable.ic_swap_horiz_blue));
454         }
455     }
456 
disableSwitchPref()457     private void disableSwitchPref() {
458         mSwitchPref.setChecked(false);
459         mSwitchPref.setTitle(R.string.interact_across_profiles_switch_disabled);
460         final ImageView horizontalArrowIcon = mHeader.findViewById(R.id.entity_header_swap_horiz);
461         if (horizontalArrowIcon != null) {
462             horizontalArrowIcon.setImageDrawable(
463                     mContext.getDrawable(R.drawable.ic_swap_horiz_grey));
464         }
465     }
466 
467     @Override
createDialog(int id, int errorCode)468     protected AlertDialog createDialog(int id, int errorCode) {
469         return null;
470     }
471 
472     @Override
getMetricsCategory()473     public int getMetricsCategory() {
474         return SettingsEnums.INTERACT_ACROSS_PROFILES;
475     }
476 
launchedByApp()477     private boolean launchedByApp() {
478         final Bundle bundle = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGS);
479         if (bundle == null) {
480             return false;
481         }
482         final Intent intent = (Intent) bundle.get(INTENT_KEY);
483         if (intent == null) {
484             return false;
485         }
486         return ACTION_MANAGE_CROSS_PROFILE_ACCESS.equals(intent.getAction());
487     }
488 }
489