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