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