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 (!isCrossProfilePackageAllowlisted(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 (!isCrossProfilePackageAllowlisted(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 isCrossProfilePackageAllowlisted(String packageName)420 private boolean isCrossProfilePackageAllowlisted(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