• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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.internal.app;
18 
19 import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
20 import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
21 import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
22 
23 import static com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER;
24 import static com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE;
25 
26 import android.annotation.Nullable;
27 import android.app.Activity;
28 import android.app.ActivityThread;
29 import android.app.AppGlobals;
30 import android.app.admin.DevicePolicyManager;
31 import android.compat.annotation.UnsupportedAppUsage;
32 import android.content.ComponentName;
33 import android.content.ContentResolver;
34 import android.content.Intent;
35 import android.content.pm.ActivityInfo;
36 import android.content.pm.IPackageManager;
37 import android.content.pm.PackageManager;
38 import android.content.pm.ResolveInfo;
39 import android.content.pm.UserInfo;
40 import android.metrics.LogMaker;
41 import android.os.Build;
42 import android.os.Bundle;
43 import android.os.RemoteException;
44 import android.os.UserHandle;
45 import android.os.UserManager;
46 import android.provider.Settings;
47 import android.util.Slog;
48 import android.widget.Toast;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.internal.logging.MetricsLogger;
52 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
53 
54 import java.util.Arrays;
55 import java.util.HashSet;
56 import java.util.List;
57 import java.util.Set;
58 import java.util.concurrent.CompletableFuture;
59 import java.util.concurrent.ExecutorService;
60 import java.util.concurrent.Executors;
61 
62 /**
63  * This is used in conjunction with
64  * {@link DevicePolicyManager#addCrossProfileIntentFilter} to enable intents to
65  * be passed in and out of a managed profile.
66  */
67 public class IntentForwarderActivity extends Activity  {
68     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
69     public static String TAG = "IntentForwarderActivity";
70 
71     public static String FORWARD_INTENT_TO_PARENT
72             = "com.android.internal.app.ForwardIntentToParent";
73 
74     public static String FORWARD_INTENT_TO_MANAGED_PROFILE
75             = "com.android.internal.app.ForwardIntentToManagedProfile";
76 
77     private static final Set<String> ALLOWED_TEXT_MESSAGE_SCHEMES
78             = new HashSet<>(Arrays.asList("sms", "smsto", "mms", "mmsto"));
79 
80     private static final String TEL_SCHEME = "tel";
81 
82     private static final ComponentName RESOLVER_COMPONENT_NAME =
83             new ComponentName("android", ResolverActivity.class.getName());
84 
85     private Injector mInjector;
86 
87     private MetricsLogger mMetricsLogger;
88     protected ExecutorService mExecutorService;
89 
90     @Override
onDestroy()91     protected void onDestroy() {
92         super.onDestroy();
93         mExecutorService.shutdown();
94     }
95 
96     @Override
onCreate(Bundle savedInstanceState)97     protected void onCreate(Bundle savedInstanceState) {
98         super.onCreate(savedInstanceState);
99         mInjector = createInjector();
100         mExecutorService = Executors.newSingleThreadExecutor();
101 
102         Intent intentReceived = getIntent();
103         String className = intentReceived.getComponent().getClassName();
104         final int targetUserId;
105         final String userMessage;
106         if (className.equals(FORWARD_INTENT_TO_PARENT)) {
107             userMessage = getForwardToPersonalMessage();
108             targetUserId = getProfileParent();
109 
110             getMetricsLogger().write(
111                     new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE)
112                     .setSubtype(MetricsEvent.PARENT_PROFILE));
113         } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
114             userMessage = getForwardToWorkMessage();
115             targetUserId = getManagedProfile();
116 
117             getMetricsLogger().write(
118                     new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE)
119                     .setSubtype(MetricsEvent.MANAGED_PROFILE));
120         } else {
121             Slog.wtf(TAG, IntentForwarderActivity.class.getName() + " cannot be called directly");
122             userMessage = null;
123             targetUserId = UserHandle.USER_NULL;
124         }
125         if (targetUserId == UserHandle.USER_NULL) {
126             // This covers the case where there is no parent / managed profile.
127             finish();
128             return;
129         }
130         if (Intent.ACTION_CHOOSER.equals(intentReceived.getAction())) {
131             launchChooserActivityWithCorrectTab(intentReceived, className);
132             return;
133         }
134 
135         final int callingUserId = getUserId();
136         final Intent newIntent = canForward(intentReceived, getUserId(), targetUserId,
137                 mInjector.getIPackageManager(), getContentResolver());
138 
139         if (newIntent == null) {
140             Slog.wtf(TAG, "the intent: " + intentReceived + " cannot be forwarded from user "
141                     + callingUserId + " to user " + targetUserId);
142             finish();
143             return;
144         }
145 
146         newIntent.prepareToLeaveUser(callingUserId);
147         final CompletableFuture<ResolveInfo> targetResolveInfoFuture =
148                 mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId);
149         targetResolveInfoFuture
150                 .thenApplyAsync(targetResolveInfo -> {
151                     if (isResolverActivityResolveInfo(targetResolveInfo)) {
152                         launchResolverActivityWithCorrectTab(intentReceived, className, newIntent,
153                                 callingUserId, targetUserId);
154                         return targetResolveInfo;
155                     }
156                     startActivityAsCaller(newIntent, targetUserId);
157                     return targetResolveInfo;
158                 }, mExecutorService)
159                 .thenAcceptAsync(result -> {
160                     maybeShowDisclosure(intentReceived, result, userMessage);
161                     finish();
162                 }, getApplicationContext().getMainExecutor());
163     }
164 
getForwardToPersonalMessage()165     private String getForwardToPersonalMessage() {
166         return getSystemService(DevicePolicyManager.class).getResources().getString(
167                 FORWARD_INTENT_TO_PERSONAL,
168                 () -> getString(com.android.internal.R.string.forward_intent_to_owner));
169     }
170 
getForwardToWorkMessage()171     private String getForwardToWorkMessage() {
172         return getSystemService(DevicePolicyManager.class).getResources().getString(
173                 FORWARD_INTENT_TO_WORK,
174                 () -> getString(com.android.internal.R.string.forward_intent_to_work));
175     }
176 
isIntentForwarderResolveInfo(ResolveInfo resolveInfo)177     private boolean isIntentForwarderResolveInfo(ResolveInfo resolveInfo) {
178         if (resolveInfo == null) {
179             return false;
180         }
181         ActivityInfo activityInfo = resolveInfo.activityInfo;
182         if (activityInfo == null) {
183             return false;
184         }
185         if (!"android".equals(activityInfo.packageName)) {
186             return false;
187         }
188         return activityInfo.name.equals(FORWARD_INTENT_TO_PARENT)
189                 || activityInfo.name.equals(FORWARD_INTENT_TO_MANAGED_PROFILE);
190     }
191 
isResolverActivityResolveInfo(@ullable ResolveInfo resolveInfo)192     private boolean isResolverActivityResolveInfo(@Nullable ResolveInfo resolveInfo) {
193         return resolveInfo != null
194                 && resolveInfo.activityInfo != null
195                 && RESOLVER_COMPONENT_NAME.equals(resolveInfo.activityInfo.getComponentName());
196     }
197 
maybeShowDisclosure( Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message)198     private void maybeShowDisclosure(
199             Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message) {
200         if (shouldShowDisclosure(resolveInfo, intentReceived) && message != null) {
201             mInjector.showToast(message, Toast.LENGTH_LONG);
202         }
203     }
204 
startActivityAsCaller(Intent newIntent, int userId)205     private void startActivityAsCaller(Intent newIntent, int userId) {
206         try {
207             startActivityAsCaller(
208                     newIntent,
209                     /* options= */ null,
210                     /* ignoreTargetSecurity= */ false,
211                     userId);
212         } catch (RuntimeException e) {
213             Slog.wtf(TAG, "Unable to launch as UID " + getLaunchedFromUid() + " package "
214                     + getLaunchedFromPackage() + ", while running in "
215                     + ActivityThread.currentProcessName(), e);
216         }
217     }
218 
launchChooserActivityWithCorrectTab(Intent intentReceived, String className)219     private void launchChooserActivityWithCorrectTab(Intent intentReceived, String className) {
220         // When showing the sharesheet, instead of forwarding to the other profile,
221         // we launch the sharesheet in the current user and select the other tab.
222         // This fixes b/152866292 where the user can not go back to the original profile
223         // when cross-profile intents are disabled.
224         int selectedProfile = findSelectedProfile(className);
225         sanitizeIntent(intentReceived);
226         intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
227         Intent innerIntent = intentReceived.getParcelableExtra(Intent.EXTRA_INTENT);
228         if (innerIntent == null) {
229             Slog.wtf(TAG, "Cannot start a chooser intent with no extra " + Intent.EXTRA_INTENT);
230             return;
231         }
232         sanitizeIntent(innerIntent);
233         startActivityAsCaller(intentReceived, null, false, getUserId());
234         finish();
235     }
236 
launchResolverActivityWithCorrectTab(Intent intentReceived, String className, Intent newIntent, int callingUserId, int targetUserId)237     private void launchResolverActivityWithCorrectTab(Intent intentReceived, String className,
238             Intent newIntent, int callingUserId, int targetUserId) {
239         // When showing the intent resolver, instead of forwarding to the other profile,
240         // we launch it in the current user and select the other tab. This fixes b/155874820.
241         //
242         // In the case when there are 0 targets in the current profile and >1 apps in the other
243         // profile, the package manager launches the intent resolver in the other profile.
244         // If that's the case, we launch the resolver in the target user instead (other profile).
245         ResolveInfo callingResolveInfo = mInjector.resolveActivityAsUser(
246                 newIntent, MATCH_DEFAULT_ONLY, callingUserId).join();
247         int userId = isIntentForwarderResolveInfo(callingResolveInfo)
248                 ? targetUserId : callingUserId;
249         int selectedProfile = findSelectedProfile(className);
250         sanitizeIntent(intentReceived);
251         intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
252         intentReceived.putExtra(EXTRA_CALLING_USER, UserHandle.of(callingUserId));
253         startActivityAsCaller(intentReceived, null, false, userId);
254         finish();
255     }
256 
findSelectedProfile(String className)257     private int findSelectedProfile(String className) {
258         if (className.equals(FORWARD_INTENT_TO_PARENT)) {
259             return ChooserActivity.PROFILE_PERSONAL;
260         } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
261             return ChooserActivity.PROFILE_WORK;
262         }
263         return -1;
264     }
265 
shouldShowDisclosure(@ullable ResolveInfo ri, Intent intent)266     private boolean shouldShowDisclosure(@Nullable ResolveInfo ri, Intent intent) {
267         if (!isDeviceProvisioned()) {
268             return false;
269         }
270         if (ri == null || ri.activityInfo == null) {
271             return true;
272         }
273         if (ri.activityInfo.applicationInfo.isSystemApp()
274                 && (isDialerIntent(intent) || isTextMessageIntent(intent))) {
275             return false;
276         }
277         return !isTargetResolverOrChooserActivity(ri.activityInfo);
278     }
279 
isDeviceProvisioned()280     private boolean isDeviceProvisioned() {
281         return Settings.Global.getInt(getContentResolver(),
282                 Settings.Global.DEVICE_PROVISIONED, /* def= */ 0) != 0;
283     }
284 
isTextMessageIntent(Intent intent)285     private boolean isTextMessageIntent(Intent intent) {
286         return (Intent.ACTION_SENDTO.equals(intent.getAction()) || isViewActionIntent(intent))
287                 && ALLOWED_TEXT_MESSAGE_SCHEMES.contains(intent.getScheme());
288     }
289 
isDialerIntent(Intent intent)290     private boolean isDialerIntent(Intent intent) {
291         return Intent.ACTION_DIAL.equals(intent.getAction())
292                 || Intent.ACTION_CALL.equals(intent.getAction())
293                 || Intent.ACTION_CALL_PRIVILEGED.equals(intent.getAction())
294                 || Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction())
295                 || (isViewActionIntent(intent) && TEL_SCHEME.equals(intent.getScheme()));
296     }
297 
isViewActionIntent(Intent intent)298     private boolean isViewActionIntent(Intent intent) {
299         return Intent.ACTION_VIEW.equals(intent.getAction())
300                 && intent.hasCategory(Intent.CATEGORY_BROWSABLE);
301     }
302 
isTargetResolverOrChooserActivity(ActivityInfo activityInfo)303     private boolean isTargetResolverOrChooserActivity(ActivityInfo activityInfo) {
304         if (!"android".equals(activityInfo.packageName)) {
305             return false;
306         }
307         return ResolverActivity.class.getName().equals(activityInfo.name)
308             || ChooserActivity.class.getName().equals(activityInfo.name);
309     }
310 
311     /**
312      * Check whether the intent can be forwarded to target user. Return the intent used for
313      * forwarding if it can be forwarded, {@code null} otherwise.
314      */
canForward(Intent incomingIntent, int sourceUserId, int targetUserId, IPackageManager packageManager, ContentResolver contentResolver)315     static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
316             IPackageManager packageManager, ContentResolver contentResolver)  {
317         Intent forwardIntent = new Intent(incomingIntent);
318         forwardIntent.addFlags(
319                 Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
320         sanitizeIntent(forwardIntent);
321 
322         Intent intentToCheck = forwardIntent;
323         if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) {
324             return null;
325         }
326         if (forwardIntent.getSelector() != null) {
327             intentToCheck = forwardIntent.getSelector();
328         }
329         String resolvedType = intentToCheck.resolveTypeIfNeeded(contentResolver);
330         sanitizeIntent(intentToCheck);
331         try {
332             if (packageManager.canForwardTo(
333                     intentToCheck, resolvedType, sourceUserId, targetUserId)) {
334                 return forwardIntent;
335             }
336         } catch (RemoteException e) {
337             Slog.e(TAG, "PackageManagerService is dead?");
338         }
339         return null;
340     }
341 
342     /**
343      * Returns the userId of the managed profile for this device or UserHandle.USER_NULL if there is
344      * no managed profile.
345      *
346      * TODO: Remove the assumption that there is only one managed profile
347      * on the device.
348      */
getManagedProfile()349     private int getManagedProfile() {
350         List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId());
351         for (UserInfo userInfo : relatedUsers) {
352             if (userInfo.isManagedProfile()) return userInfo.id;
353         }
354         Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE
355                 + " has been called, but there is no managed profile");
356         return UserHandle.USER_NULL;
357     }
358 
359     /**
360      * Returns the userId of the profile parent or UserHandle.USER_NULL if there is
361      * no parent.
362      */
getProfileParent()363     private int getProfileParent() {
364         UserInfo parent = mInjector.getUserManager().getProfileParent(UserHandle.myUserId());
365         if (parent == null) {
366             Slog.wtf(TAG, FORWARD_INTENT_TO_PARENT
367                     + " has been called, but there is no parent");
368             return UserHandle.USER_NULL;
369         }
370         return parent.id;
371     }
372 
373     /**
374      * Sanitize the intent in place.
375      */
sanitizeIntent(Intent intent)376     private static void sanitizeIntent(Intent intent) {
377         // Apps should not be allowed to target a specific package/ component in the target user.
378         intent.setPackage(null);
379         intent.setComponent(null);
380     }
381 
getMetricsLogger()382     protected MetricsLogger getMetricsLogger() {
383         if (mMetricsLogger == null) {
384             mMetricsLogger = new MetricsLogger();
385         }
386         return mMetricsLogger;
387     }
388 
389     @VisibleForTesting
createInjector()390     protected Injector createInjector() {
391         return new InjectorImpl();
392     }
393 
394     private class InjectorImpl implements Injector {
395 
396         @Override
getIPackageManager()397         public IPackageManager getIPackageManager() {
398             return AppGlobals.getPackageManager();
399         }
400 
401         @Override
getUserManager()402         public UserManager getUserManager() {
403             return getSystemService(UserManager.class);
404         }
405 
406         @Override
getPackageManager()407         public PackageManager getPackageManager() {
408             return IntentForwarderActivity.this.getPackageManager();
409         }
410 
411         @Override
412         @Nullable
resolveActivityAsUser( Intent intent, int flags, int userId)413         public CompletableFuture<ResolveInfo> resolveActivityAsUser(
414                 Intent intent, int flags, int userId) {
415             return CompletableFuture.supplyAsync(
416                     () -> getPackageManager().resolveActivityAsUser(intent, flags, userId));
417         }
418 
419         @Override
showToast(String message, int duration)420         public void showToast(String message, int duration) {
421             Toast.makeText(IntentForwarderActivity.this, message, duration).show();
422         }
423     }
424 
425     public interface Injector {
getIPackageManager()426         IPackageManager getIPackageManager();
427 
getUserManager()428         UserManager getUserManager();
429 
getPackageManager()430         PackageManager getPackageManager();
431 
resolveActivityAsUser(Intent intent, int flags, int userId)432         CompletableFuture<ResolveInfo> resolveActivityAsUser(Intent intent, int flags, int userId);
433 
showToast(String message, int duration)434         void showToast(String message, int duration);
435     }
436 }
437