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 17 package com.android.managedprovisioning.ota; 18 19 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; 20 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; 21 import static android.os.Build.VERSION_CODES.P; 22 import static android.os.Build.VERSION_CODES.Q; 23 import static android.os.Build.VERSION_CODES.R; 24 25 import static com.google.common.base.Preconditions.checkNotNull; 26 27 import android.Manifest; 28 import android.Manifest.permission; 29 import android.annotation.NonNull; 30 import android.annotation.RequiresPermission; 31 import android.annotation.SystemApi; 32 import android.app.AppOpsManager; 33 import android.app.AppOpsManager.Mode; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.content.pm.ActivityInfo; 38 import android.content.pm.CrossProfileApps; 39 import android.content.pm.ICrossProfileApps; 40 import android.content.pm.PackageInfo; 41 import android.content.pm.PackageManager; 42 import android.graphics.drawable.ColorDrawable; 43 import android.graphics.drawable.Drawable; 44 import android.os.Process; 45 import android.os.UserHandle; 46 import android.text.TextUtils; 47 48 import com.google.common.collect.ImmutableList; 49 import com.google.common.collect.Iterables; 50 51 import org.robolectric.annotation.Implementation; 52 import org.robolectric.annotation.Implements; 53 import org.robolectric.annotation.Resetter; 54 import org.robolectric.shadows.ShadowCrossProfileApps; 55 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Collection; 59 import java.util.Collections; 60 import java.util.HashMap; 61 import java.util.HashSet; 62 import java.util.LinkedHashSet; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Objects; 66 import java.util.Set; 67 import java.util.stream.Collectors; 68 69 import javax.annotation.Nullable; 70 71 /** Robolectric implementation of {@link CrossProfileApps}. 72 * This class is debt, and it seems intents should be used in tests instead, 73 * as exposed by robolectric's version of this Shadows. 74 */ 75 @Implements(value = CrossProfileApps.class, minSdk = P) 76 public class ExtendsShadowCrossProfileApps { 77 78 // BEGIN-INTERNAL 79 private final static String INTERACT_ACROSS_PROFILES_APPOP = AppOpsManager.permissionToOp( 80 Manifest.permission.INTERACT_ACROSS_PROFILES); 81 private static final Set<String> configurableInteractAcrossProfilePackages = new HashSet<>(); 82 // END-INTERNAL 83 84 private final Set<UserHandle> targetUserProfiles = new LinkedHashSet<>(); 85 private final List<StartedMainActivity> startedMainActivities = new ArrayList<>(); 86 private final List<StartedActivity> startedActivities = 87 Collections.synchronizedList(new ArrayList<>()); 88 private final Map<String, Integer> packageNameAppOpModes = new HashMap<>(); 89 90 private Context context; 91 private PackageManager packageManager; 92 93 @Implementation __constructor__(Context context, ICrossProfileApps service)94 protected void __constructor__(Context context, ICrossProfileApps service) { 95 this.context = context; 96 this.packageManager = context.getPackageManager(); 97 } 98 99 /** 100 * Returns a list of {@link UserHandle}s currently accessible. This list is populated from calls 101 * to {@link #addTargetUserProfile(UserHandle)}. 102 */ 103 @Implementation getTargetUserProfiles()104 protected List<UserHandle> getTargetUserProfiles() { 105 return ImmutableList.copyOf(targetUserProfiles); 106 } 107 108 /** 109 * Returns a {@link Drawable} that can be shown for profile switching, which is guaranteed to 110 * always be the same for a particular user and to be distinct between users. 111 */ 112 @Implementation getProfileSwitchingIconDrawable(UserHandle userHandle)113 protected Drawable getProfileSwitchingIconDrawable(UserHandle userHandle) { 114 verifyCanAccessUser(userHandle); 115 return new ColorDrawable(userHandle.getIdentifier()); 116 } 117 118 /** 119 * Returns a {@link CharSequence} that can be shown as a label for profile switching, which is 120 * guaranteed to always be the same for a particular user and to be distinct between users. 121 */ 122 @Implementation getProfileSwitchingLabel(UserHandle userHandle)123 protected CharSequence getProfileSwitchingLabel(UserHandle userHandle) { 124 verifyCanAccessUser(userHandle); 125 return "Switch to " + userHandle; 126 } 127 128 /** 129 * Simulates starting the main activity specified in the specified profile, performing the same 130 * security checks done by the real {@link CrossProfileApps}. 131 * 132 * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()} 133 * ()}. 134 */ 135 @Implementation startMainActivity(ComponentName componentName, UserHandle targetUser)136 protected void startMainActivity(ComponentName componentName, UserHandle targetUser) { 137 verifyCanAccessUser(targetUser); 138 verifyActivityInManifest(componentName, /* requireMainActivity= */ true); 139 startedMainActivities.add(new StartedMainActivity(componentName, targetUser)); 140 startedActivities.add(new StartedActivity(componentName, targetUser)); 141 } 142 143 /** 144 * Simulates starting the activity specified in the specified profile, performing the same 145 * security checks done by the real {@link CrossProfileApps}. 146 * 147 * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()} 148 * ()}. 149 */ 150 @Implementation(minSdk = Q) 151 @SystemApi 152 @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) startActivity(ComponentName componentName, UserHandle targetUser)153 protected void startActivity(ComponentName componentName, UserHandle targetUser) { 154 verifyCanAccessUser(targetUser); 155 verifyActivityInManifest(componentName, /* requireMainActivity= */ false); 156 verifyHasInteractAcrossProfilesPermission(); 157 startedActivities.add(new StartedActivity(componentName, targetUser)); 158 } 159 160 /** Adds {@code userHandle} to the list of accessible handles. */ addTargetUserProfile(UserHandle userHandle)161 public void addTargetUserProfile(UserHandle userHandle) { 162 if (userHandle.equals(Process.myUserHandle())) { 163 throw new IllegalArgumentException("Cannot target current user"); 164 } 165 targetUserProfiles.add(userHandle); 166 } 167 168 /** Removes {@code userHandle} from the list of accessible handles, if present. */ removeTargetUserProfile(UserHandle userHandle)169 public void removeTargetUserProfile(UserHandle userHandle) { 170 if (userHandle.equals(Process.myUserHandle())) { 171 throw new IllegalArgumentException("Cannot target current user"); 172 } 173 targetUserProfiles.remove(userHandle); 174 } 175 176 /** Clears the list of accessible handles. */ clearTargetUserProfiles()177 public void clearTargetUserProfiles() { 178 targetUserProfiles.clear(); 179 } 180 181 /** 182 * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link 183 * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, wrapped in {@link 184 * StartedMainActivity}. 185 * 186 * @deprecated Use {@link #peekNextStartedActivity()} instead. 187 */ 188 @Nullable 189 @Deprecated peekNextStartedMainActivity()190 public StartedMainActivity peekNextStartedMainActivity() { 191 if (startedMainActivities.isEmpty()) { 192 return null; 193 } else { 194 return Iterables.getLast(startedMainActivities); 195 } 196 } 197 198 /** 199 * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link 200 * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link 201 * CrossProfileApps#startActivity(ComponentName, UserHandle)}, wrapped in {@link StartedActivity}. 202 */ 203 @Nullable peekNextStartedActivity()204 public StartedActivity peekNextStartedActivity() { 205 if (startedActivities.isEmpty()) { 206 return null; 207 } else { 208 return Iterables.getLast(startedActivities); 209 } 210 } 211 212 /** 213 * Consumes the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link 214 * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link 215 * CrossProfileApps#startActivity(ComponentName, UserHandle)}, and returns it wrapped in {@link 216 * StartedActivity}. 217 */ 218 @Nullable getNextStartedActivity()219 public StartedActivity getNextStartedActivity() { 220 if (startedActivities.isEmpty()) { 221 return null; 222 } else { 223 return startedActivities.remove(startedActivities.size() - 1); 224 } 225 } 226 227 /** 228 * Clears all records of {@link StartedActivity}s from calls to {@link 229 * CrossProfileApps#startActivity(ComponentName, UserHandle)} or {@link 230 * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}. 231 */ clearNextStartedActivities()232 public void clearNextStartedActivities() { 233 startedActivities.clear(); 234 } 235 verifyCanAccessUser(UserHandle userHandle)236 private void verifyCanAccessUser(UserHandle userHandle) { 237 if (!targetUserProfiles.contains(userHandle)) { 238 throw new SecurityException( 239 "Not allowed to access " 240 + userHandle 241 + " (did you forget to call addTargetUserProfile?)"); 242 } 243 } 244 verifyHasInteractAcrossProfilesPermission()245 private void verifyHasInteractAcrossProfilesPermission() { 246 if (context.checkSelfPermission(permission.INTERACT_ACROSS_PROFILES) 247 != PackageManager.PERMISSION_GRANTED) { 248 throw new SecurityException( 249 "Attempt to launch activity without required " 250 + permission.INTERACT_ACROSS_PROFILES 251 + " permission"); 252 } 253 } 254 255 /** 256 * Ensures that {@code component} is present in the manifest as an exported and enabled activity. 257 * This check and the error thrown are the same as the check done by the real {@link 258 * CrossProfileApps}. 259 * 260 * <p>If {@code requireMainActivity} is true, then this also asserts that the activity is a 261 * launcher activity. 262 */ verifyActivityInManifest(ComponentName component, boolean requireMainActivity)263 private void verifyActivityInManifest(ComponentName component, boolean requireMainActivity) { 264 Intent launchIntent = new Intent(); 265 if (requireMainActivity) { 266 launchIntent 267 .setAction(Intent.ACTION_MAIN) 268 .addCategory(Intent.CATEGORY_LAUNCHER) 269 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) 270 .setPackage(component.getPackageName()); 271 } else { 272 launchIntent.setComponent(component); 273 } 274 275 boolean existsMatchingActivity = 276 Iterables.any( 277 packageManager.queryIntentActivities( 278 launchIntent, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE), 279 resolveInfo -> { 280 ActivityInfo activityInfo = resolveInfo.activityInfo; 281 return TextUtils.equals(activityInfo.packageName, component.getPackageName()) 282 && TextUtils.equals(activityInfo.name, component.getClassName()) 283 && activityInfo.exported; 284 }); 285 if (!existsMatchingActivity) { 286 throw new SecurityException( 287 "Attempt to launch activity without " 288 + " category Intent.CATEGORY_LAUNCHER or activity is not exported" 289 + component); 290 } 291 } 292 293 // BEGIN-INTERNAL 294 @Implementation(minSdk = R) 295 @RequiresPermission( 296 allOf={android.Manifest.permission.MANAGE_APP_OPS_MODES, 297 android.Manifest.permission.INTERACT_ACROSS_USERS}) setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode)298 protected void setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode) { 299 packageNameAppOpModes.put(packageName, newMode); 300 } 301 302 /** 303 * Returns the app-op mode associated with the given package name. If not set, returns {@code 304 * null}. 305 */ 306 @Nullable getInteractAcrossProfilesAppOp(String packageName)307 public @Mode Integer getInteractAcrossProfilesAppOp(String packageName) { 308 return packageNameAppOpModes.get(packageName); 309 } 310 addCrossProfilePackage(String packageName)311 public void addCrossProfilePackage(String packageName){ 312 configurableInteractAcrossProfilePackages.add(packageName); 313 } 314 315 @Implementation(minSdk = R) resetInteractAcrossProfilesAppOps( @onNull Collection<String> previousCrossProfilePackages, @NonNull Set<String> newCrossProfilePackages)316 protected void resetInteractAcrossProfilesAppOps( 317 @NonNull Collection<String> previousCrossProfilePackages, 318 @NonNull Set<String> newCrossProfilePackages) { 319 320 final List<String> unsetCrossProfilePackages = 321 previousCrossProfilePackages.stream() 322 .filter(packageName -> !newCrossProfilePackages.contains(packageName)) 323 .collect(Collectors.toList()); 324 325 for (String packageName : unsetCrossProfilePackages) { 326 if (!canConfigureInteractAcrossProfiles(packageName)) { 327 setInteractAcrossProfilesAppOp(packageName, 328 AppOpsManager.opToDefaultMode(INTERACT_ACROSS_PROFILES_APPOP)); 329 } 330 } 331 } 332 333 // BEGIN-INTERNAL 334 @Implementation(minSdk = R) clearInteractAcrossProfilesAppOps()335 protected void clearInteractAcrossProfilesAppOps() { 336 findAllPackageNames().forEach( 337 packageName -> setInteractAcrossProfilesAppOp( 338 packageName, AppOpsManager.opToDefaultMode(INTERACT_ACROSS_PROFILES_APPOP))); 339 } 340 findAllPackageNames()341 private List<String> findAllPackageNames() { 342 return context.getPackageManager() 343 .getInstalledApplications(/* flags= */ 0) 344 .stream() 345 .map(applicationInfo -> applicationInfo.packageName) 346 .collect(Collectors.toList()); 347 } 348 // END-INTERNAL 349 350 @Implementation canConfigureInteractAcrossProfiles(@onNull String packageName)351 protected boolean canConfigureInteractAcrossProfiles(@NonNull String packageName) { 352 return configurableInteractAcrossProfilePackages.contains(packageName); 353 } 354 355 @Implementation canUserAttemptToConfigureInteractAcrossProfiles(@onNull String packageName)356 protected boolean canUserAttemptToConfigureInteractAcrossProfiles(@NonNull String packageName) { 357 PackageInfo packageInfo; 358 try { 359 packageInfo = packageManager.getPackageInfo(packageName, /* flags= */ 0); 360 } catch (PackageManager.NameNotFoundException e) { 361 return false; 362 } 363 if (packageInfo == null || packageInfo.requestedPermissions == null) { 364 return false; 365 } 366 return Arrays.asList(packageInfo.requestedPermissions).contains( 367 Manifest.permission.INTERACT_ACROSS_PROFILES); 368 } 369 370 @Resetter reset()371 public static void reset() { 372 configurableInteractAcrossProfilePackages.clear(); 373 } 374 // END-INTERNAL 375 376 /** 377 * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, 378 * UserHandle)}. 379 * 380 * @deprecated Use {@link #peekNextStartedActivity()} and {@link StartedActivity} instead. 381 */ 382 @Deprecated 383 public static class StartedMainActivity { 384 385 private final ComponentName componentName; 386 private final UserHandle userHandle; 387 StartedMainActivity(ComponentName componentName, UserHandle userHandle)388 public StartedMainActivity(ComponentName componentName, UserHandle userHandle) { 389 this.componentName = checkNotNull(componentName); 390 this.userHandle = checkNotNull(userHandle); 391 } 392 getComponentName()393 public ComponentName getComponentName() { 394 return componentName; 395 } 396 getUserHandle()397 public UserHandle getUserHandle() { 398 return userHandle; 399 } 400 401 @Override equals(Object o)402 public boolean equals(Object o) { 403 if (this == o) { 404 return true; 405 } 406 if (o == null || getClass() != o.getClass()) { 407 return false; 408 } 409 StartedMainActivity that = (StartedMainActivity) o; 410 return Objects.equals(componentName, that.componentName) 411 && Objects.equals(userHandle, that.userHandle); 412 } 413 414 @Override hashCode()415 public int hashCode() { 416 return Objects.hash(componentName, userHandle); 417 } 418 } 419 420 /** 421 * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, 422 * UserHandle)} or {@link #startActivity(ComponentName, UserHandle)}. 423 */ 424 public static final class StartedActivity { 425 426 private final ComponentName componentName; 427 private final UserHandle userHandle; 428 StartedActivity(ComponentName componentName, UserHandle userHandle)429 public StartedActivity(ComponentName componentName, UserHandle userHandle) { 430 this.componentName = checkNotNull(componentName); 431 this.userHandle = checkNotNull(userHandle); 432 } 433 getComponentName()434 public ComponentName getComponentName() { 435 return componentName; 436 } 437 getUserHandle()438 public UserHandle getUserHandle() { 439 return userHandle; 440 } 441 442 @Override equals(Object o)443 public boolean equals(Object o) { 444 if (this == o) { 445 return true; 446 } 447 if (o == null || getClass() != o.getClass()) { 448 return false; 449 } 450 StartedActivity that = (StartedActivity) o; 451 return Objects.equals(componentName, that.componentName) 452 && Objects.equals(userHandle, that.userHandle); 453 } 454 455 @Override hashCode()456 public int hashCode() { 457 return Objects.hash(componentName, userHandle); 458 } 459 } 460 } 461