/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.managedprovisioning.ota; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.os.Build.VERSION_CODES.P; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static com.google.common.base.Preconditions.checkNotNull; import android.Manifest; import android.Manifest.permission; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.app.AppOpsManager; import android.app.AppOpsManager.Mode; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.CrossProfileApps; import android.content.pm.ICrossProfileApps; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; import org.robolectric.shadows.ShadowCrossProfileApps; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; /** Robolectric implementation of {@link CrossProfileApps}. * This class is debt, and it seems intents should be used in tests instead, * as exposed by robolectric's version of this Shadows. */ @Implements(value = CrossProfileApps.class, minSdk = P) public class ExtendsShadowCrossProfileApps { // BEGIN-INTERNAL private final static String INTERACT_ACROSS_PROFILES_APPOP = AppOpsManager.permissionToOp( Manifest.permission.INTERACT_ACROSS_PROFILES); private static final Set configurableInteractAcrossProfilePackages = new HashSet<>(); // END-INTERNAL private final Set targetUserProfiles = new LinkedHashSet<>(); private final List startedMainActivities = new ArrayList<>(); private final List startedActivities = Collections.synchronizedList(new ArrayList<>()); private final Map packageNameAppOpModes = new HashMap<>(); private Context context; private PackageManager packageManager; @Implementation protected void __constructor__(Context context, ICrossProfileApps service) { this.context = context; this.packageManager = context.getPackageManager(); } /** * Returns a list of {@link UserHandle}s currently accessible. This list is populated from calls * to {@link #addTargetUserProfile(UserHandle)}. */ @Implementation protected List getTargetUserProfiles() { return ImmutableList.copyOf(targetUserProfiles); } /** * Returns a {@link Drawable} that can be shown for profile switching, which is guaranteed to * always be the same for a particular user and to be distinct between users. */ @Implementation protected Drawable getProfileSwitchingIconDrawable(UserHandle userHandle) { verifyCanAccessUser(userHandle); return new ColorDrawable(userHandle.getIdentifier()); } /** * Returns a {@link CharSequence} that can be shown as a label for profile switching, which is * guaranteed to always be the same for a particular user and to be distinct between users. */ @Implementation protected CharSequence getProfileSwitchingLabel(UserHandle userHandle) { verifyCanAccessUser(userHandle); return "Switch to " + userHandle; } /** * Simulates starting the main activity specified in the specified profile, performing the same * security checks done by the real {@link CrossProfileApps}. * *

The most recent main activity started can be queried by {@link #peekNextStartedActivity()} * ()}. */ @Implementation protected void startMainActivity(ComponentName componentName, UserHandle targetUser) { verifyCanAccessUser(targetUser); verifyActivityInManifest(componentName, /* requireMainActivity= */ true); startedMainActivities.add(new StartedMainActivity(componentName, targetUser)); startedActivities.add(new StartedActivity(componentName, targetUser)); } /** * Simulates starting the activity specified in the specified profile, performing the same * security checks done by the real {@link CrossProfileApps}. * *

The most recent main activity started can be queried by {@link #peekNextStartedActivity()} * ()}. */ @Implementation(minSdk = Q) @SystemApi @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) protected void startActivity(ComponentName componentName, UserHandle targetUser) { verifyCanAccessUser(targetUser); verifyActivityInManifest(componentName, /* requireMainActivity= */ false); verifyHasInteractAcrossProfilesPermission(); startedActivities.add(new StartedActivity(componentName, targetUser)); } /** Adds {@code userHandle} to the list of accessible handles. */ public void addTargetUserProfile(UserHandle userHandle) { if (userHandle.equals(Process.myUserHandle())) { throw new IllegalArgumentException("Cannot target current user"); } targetUserProfiles.add(userHandle); } /** Removes {@code userHandle} from the list of accessible handles, if present. */ public void removeTargetUserProfile(UserHandle userHandle) { if (userHandle.equals(Process.myUserHandle())) { throw new IllegalArgumentException("Cannot target current user"); } targetUserProfiles.remove(userHandle); } /** Clears the list of accessible handles. */ public void clearTargetUserProfiles() { targetUserProfiles.clear(); } /** * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, wrapped in {@link * StartedMainActivity}. * * @deprecated Use {@link #peekNextStartedActivity()} instead. */ @Nullable @Deprecated public StartedMainActivity peekNextStartedMainActivity() { if (startedMainActivities.isEmpty()) { return null; } else { return Iterables.getLast(startedMainActivities); } } /** * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link * CrossProfileApps#startActivity(ComponentName, UserHandle)}, wrapped in {@link StartedActivity}. */ @Nullable public StartedActivity peekNextStartedActivity() { if (startedActivities.isEmpty()) { return null; } else { return Iterables.getLast(startedActivities); } } /** * Consumes the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link * CrossProfileApps#startActivity(ComponentName, UserHandle)}, and returns it wrapped in {@link * StartedActivity}. */ @Nullable public StartedActivity getNextStartedActivity() { if (startedActivities.isEmpty()) { return null; } else { return startedActivities.remove(startedActivities.size() - 1); } } /** * Clears all records of {@link StartedActivity}s from calls to {@link * CrossProfileApps#startActivity(ComponentName, UserHandle)} or {@link * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}. */ public void clearNextStartedActivities() { startedActivities.clear(); } private void verifyCanAccessUser(UserHandle userHandle) { if (!targetUserProfiles.contains(userHandle)) { throw new SecurityException( "Not allowed to access " + userHandle + " (did you forget to call addTargetUserProfile?)"); } } private void verifyHasInteractAcrossProfilesPermission() { if (context.checkSelfPermission(permission.INTERACT_ACROSS_PROFILES) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException( "Attempt to launch activity without required " + permission.INTERACT_ACROSS_PROFILES + " permission"); } } /** * Ensures that {@code component} is present in the manifest as an exported and enabled activity. * This check and the error thrown are the same as the check done by the real {@link * CrossProfileApps}. * *

If {@code requireMainActivity} is true, then this also asserts that the activity is a * launcher activity. */ private void verifyActivityInManifest(ComponentName component, boolean requireMainActivity) { Intent launchIntent = new Intent(); if (requireMainActivity) { launchIntent .setAction(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_LAUNCHER) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) .setPackage(component.getPackageName()); } else { launchIntent.setComponent(component); } boolean existsMatchingActivity = Iterables.any( packageManager.queryIntentActivities( launchIntent, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE), resolveInfo -> { ActivityInfo activityInfo = resolveInfo.activityInfo; return TextUtils.equals(activityInfo.packageName, component.getPackageName()) && TextUtils.equals(activityInfo.name, component.getClassName()) && activityInfo.exported; }); if (!existsMatchingActivity) { throw new SecurityException( "Attempt to launch activity without " + " category Intent.CATEGORY_LAUNCHER or activity is not exported" + component); } } // BEGIN-INTERNAL @Implementation(minSdk = R) @RequiresPermission( allOf={android.Manifest.permission.MANAGE_APP_OPS_MODES, android.Manifest.permission.INTERACT_ACROSS_USERS}) protected void setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode) { packageNameAppOpModes.put(packageName, newMode); } /** * Returns the app-op mode associated with the given package name. If not set, returns {@code * null}. */ @Nullable public @Mode Integer getInteractAcrossProfilesAppOp(String packageName) { return packageNameAppOpModes.get(packageName); } public void addCrossProfilePackage(String packageName){ configurableInteractAcrossProfilePackages.add(packageName); } @Implementation(minSdk = R) protected void resetInteractAcrossProfilesAppOps( @NonNull Collection previousCrossProfilePackages, @NonNull Set newCrossProfilePackages) { final List unsetCrossProfilePackages = previousCrossProfilePackages.stream() .filter(packageName -> !newCrossProfilePackages.contains(packageName)) .collect(Collectors.toList()); for (String packageName : unsetCrossProfilePackages) { if (!canConfigureInteractAcrossProfiles(packageName)) { setInteractAcrossProfilesAppOp(packageName, AppOpsManager.opToDefaultMode(INTERACT_ACROSS_PROFILES_APPOP)); } } } // BEGIN-INTERNAL @Implementation(minSdk = R) protected void clearInteractAcrossProfilesAppOps() { findAllPackageNames().forEach( packageName -> setInteractAcrossProfilesAppOp( packageName, AppOpsManager.opToDefaultMode(INTERACT_ACROSS_PROFILES_APPOP))); } private List findAllPackageNames() { return context.getPackageManager() .getInstalledApplications(/* flags= */ 0) .stream() .map(applicationInfo -> applicationInfo.packageName) .collect(Collectors.toList()); } // END-INTERNAL @Implementation protected boolean canConfigureInteractAcrossProfiles(@NonNull String packageName) { return configurableInteractAcrossProfilePackages.contains(packageName); } @Implementation protected boolean canUserAttemptToConfigureInteractAcrossProfiles(@NonNull String packageName) { PackageInfo packageInfo; try { packageInfo = packageManager.getPackageInfo(packageName, /* flags= */ 0); } catch (PackageManager.NameNotFoundException e) { return false; } if (packageInfo == null || packageInfo.requestedPermissions == null) { return false; } return Arrays.asList(packageInfo.requestedPermissions).contains( Manifest.permission.INTERACT_ACROSS_PROFILES); } @Resetter public static void reset() { configurableInteractAcrossProfilePackages.clear(); } // END-INTERNAL /** * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, * UserHandle)}. * * @deprecated Use {@link #peekNextStartedActivity()} and {@link StartedActivity} instead. */ @Deprecated public static class StartedMainActivity { private final ComponentName componentName; private final UserHandle userHandle; public StartedMainActivity(ComponentName componentName, UserHandle userHandle) { this.componentName = checkNotNull(componentName); this.userHandle = checkNotNull(userHandle); } public ComponentName getComponentName() { return componentName; } public UserHandle getUserHandle() { return userHandle; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } StartedMainActivity that = (StartedMainActivity) o; return Objects.equals(componentName, that.componentName) && Objects.equals(userHandle, that.userHandle); } @Override public int hashCode() { return Objects.hash(componentName, userHandle); } } /** * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, * UserHandle)} or {@link #startActivity(ComponentName, UserHandle)}. */ public static final class StartedActivity { private final ComponentName componentName; private final UserHandle userHandle; public StartedActivity(ComponentName componentName, UserHandle userHandle) { this.componentName = checkNotNull(componentName); this.userHandle = checkNotNull(userHandle); } public ComponentName getComponentName() { return componentName; } public UserHandle getUserHandle() { return userHandle; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } StartedActivity that = (StartedActivity) o; return Objects.equals(componentName, that.componentName) && Objects.equals(userHandle, that.userHandle); } @Override public int hashCode() { return Objects.hash(componentName, userHandle); } } }