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.intentresolver;
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.intentresolver.ui.viewmodel.ResolverRequestReaderKt.EXTRA_CALLING_USER;
24 import static com.android.intentresolver.ui.viewmodel.ResolverRequestReaderKt.EXTRA_SELECTED_PROFILE;
25 
26 import android.app.Activity;
27 import android.app.ActivityThread;
28 import android.app.AppGlobals;
29 import android.app.admin.DevicePolicyManager;
30 import android.content.ComponentName;
31 import android.content.ContentResolver;
32 import android.content.Intent;
33 import android.content.pm.ActivityInfo;
34 import android.content.pm.IPackageManager;
35 import android.content.pm.PackageManager;
36 import android.content.pm.ResolveInfo;
37 import android.content.pm.UserInfo;
38 import android.metrics.LogMaker;
39 import android.os.Bundle;
40 import android.os.RemoteException;
41 import android.os.UserHandle;
42 import android.os.UserManager;
43 import android.provider.Settings;
44 import android.util.Slog;
45 import android.widget.Toast;
46 
47 import androidx.annotation.Nullable;
48 
49 import com.android.intentresolver.profiles.MultiProfilePagerAdapter;
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     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 String userMessage;
105         if (className.equals(FORWARD_INTENT_TO_PARENT)) {
106             userMessage = getForwardToPersonalMessage();
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             userMessage = getForwardToWorkMessage();
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             userMessage = null;
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, userMessage);
160                     finish();
161                 }, getApplicationContext().getMainExecutor());
162     }
163 
getForwardToPersonalMessage()164     private String getForwardToPersonalMessage() {
165         return getSystemService(DevicePolicyManager.class).getResources().getString(
166                 FORWARD_INTENT_TO_PERSONAL,
167                 () -> getString(R.string.forward_intent_to_owner));
168     }
169 
getForwardToWorkMessage()170     private String getForwardToWorkMessage() {
171         return getSystemService(DevicePolicyManager.class).getResources().getString(
172                 FORWARD_INTENT_TO_WORK,
173                 () -> getString(R.string.forward_intent_to_work));
174     }
175 
isIntentForwarderResolveInfo(ResolveInfo resolveInfo)176     private boolean isIntentForwarderResolveInfo(ResolveInfo resolveInfo) {
177         if (resolveInfo == null) {
178             return false;
179         }
180         ActivityInfo activityInfo = resolveInfo.activityInfo;
181         if (activityInfo == null) {
182             return false;
183         }
184         if (!"android".equals(activityInfo.packageName)) {
185             return false;
186         }
187         return activityInfo.name.equals(FORWARD_INTENT_TO_PARENT)
188                 || activityInfo.name.equals(FORWARD_INTENT_TO_MANAGED_PROFILE);
189     }
190 
isResolverActivityResolveInfo(@ullable ResolveInfo resolveInfo)191     private boolean isResolverActivityResolveInfo(@Nullable ResolveInfo resolveInfo) {
192         return resolveInfo != null
193                 && resolveInfo.activityInfo != null
194                 && RESOLVER_COMPONENT_NAME.equals(resolveInfo.activityInfo.getComponentName());
195     }
196 
maybeShowDisclosure( Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message)197     private void maybeShowDisclosure(
198             Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message) {
199         if (shouldShowDisclosure(resolveInfo, intentReceived) && message != null) {
200             mInjector.showToast(message, Toast.LENGTH_LONG);
201         }
202     }
203 
startActivityAsCaller(Intent newIntent, int userId)204     private void startActivityAsCaller(Intent newIntent, int userId) {
205         try {
206             startActivityAsCaller(
207                     newIntent,
208                     /* options= */ null,
209                     /* ignoreTargetSecurity= */ false,
210                     userId);
211         } catch (RuntimeException e) {
212             Slog.wtf(TAG, "Unable to launch as UID " + getLaunchedFromUid() + " package "
213                     + getLaunchedFromPackage() + ", while running in "
214                     + ActivityThread.currentProcessName(), e);
215         }
216     }
217 
launchChooserActivityWithCorrectTab(Intent intentReceived, String className)218     private void launchChooserActivityWithCorrectTab(Intent intentReceived, String className) {
219         // When showing the sharesheet, instead of forwarding to the other profile,
220         // we launch the sharesheet in the current user and select the other tab.
221         // This fixes b/152866292 where the user can not go back to the original profile
222         // when cross-profile intents are disabled.
223         int selectedProfile = findSelectedProfile(className);
224         sanitizeIntent(intentReceived);
225         intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
226         Intent innerIntent = intentReceived.getParcelableExtra(Intent.EXTRA_INTENT);
227         if (innerIntent == null) {
228             Slog.wtf(TAG, "Cannot start a chooser intent with no extra " + Intent.EXTRA_INTENT);
229             return;
230         }
231         sanitizeIntent(innerIntent);
232         startActivityAsCaller(intentReceived, null, false, getUserId());
233         finish();
234     }
235 
launchResolverActivityWithCorrectTab(Intent intentReceived, String className, Intent newIntent, int callingUserId, int targetUserId)236     private void launchResolverActivityWithCorrectTab(Intent intentReceived, String className,
237             Intent newIntent, int callingUserId, int targetUserId) {
238         // When showing the intent resolver, instead of forwarding to the other profile,
239         // we launch it in the current user and select the other tab. This fixes b/155874820.
240         //
241         // In the case when there are 0 targets in the current profile and >1 apps in the other
242         // profile, the package manager launches the intent resolver in the other profile.
243         // If that's the case, we launch the resolver in the target user instead (other profile).
244         ResolveInfo callingResolveInfo = mInjector.resolveActivityAsUser(
245                 newIntent, MATCH_DEFAULT_ONLY, callingUserId).join();
246         int userId = isIntentForwarderResolveInfo(callingResolveInfo)
247                 ? targetUserId : callingUserId;
248         int selectedProfile = findSelectedProfile(className);
249         sanitizeIntent(intentReceived);
250         intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
251         intentReceived.putExtra(EXTRA_CALLING_USER, UserHandle.of(callingUserId));
252         startActivityAsCaller(intentReceived, null, false, userId);
253         finish();
254     }
255 
findSelectedProfile(String className)256     private int findSelectedProfile(String className) {
257         if (className.equals(FORWARD_INTENT_TO_PARENT)) {
258             return MultiProfilePagerAdapter.PROFILE_PERSONAL;
259         } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
260             return MultiProfilePagerAdapter.PROFILE_WORK;
261         }
262         return -1;
263     }
264 
shouldShowDisclosure(@ullable ResolveInfo ri, Intent intent)265     private boolean shouldShowDisclosure(@Nullable ResolveInfo ri, Intent intent) {
266         if (!isDeviceProvisioned()) {
267             return false;
268         }
269         if (ri == null || ri.activityInfo == null) {
270             return true;
271         }
272         if (ri.activityInfo.applicationInfo.isSystemApp()
273                 && (isDialerIntent(intent) || isTextMessageIntent(intent))) {
274             return false;
275         }
276         return !isTargetResolverOrChooserActivity(ri.activityInfo);
277     }
278 
isDeviceProvisioned()279     private boolean isDeviceProvisioned() {
280         return Settings.Global.getInt(getContentResolver(),
281                 Settings.Global.DEVICE_PROVISIONED, /* def= */ 0) != 0;
282     }
283 
isTextMessageIntent(Intent intent)284     private boolean isTextMessageIntent(Intent intent) {
285         return (Intent.ACTION_SENDTO.equals(intent.getAction()) || isViewActionIntent(intent))
286                 && ALLOWED_TEXT_MESSAGE_SCHEMES.contains(intent.getScheme());
287     }
288 
isDialerIntent(Intent intent)289     private boolean isDialerIntent(Intent intent) {
290         return Intent.ACTION_DIAL.equals(intent.getAction())
291                 || Intent.ACTION_CALL.equals(intent.getAction())
292                 || Intent.ACTION_CALL_PRIVILEGED.equals(intent.getAction())
293                 || Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction())
294                 || (isViewActionIntent(intent) && TEL_SCHEME.equals(intent.getScheme()));
295     }
296 
isViewActionIntent(Intent intent)297     private boolean isViewActionIntent(Intent intent) {
298         return Intent.ACTION_VIEW.equals(intent.getAction())
299                 && intent.hasCategory(Intent.CATEGORY_BROWSABLE);
300     }
301 
isTargetResolverOrChooserActivity(ActivityInfo activityInfo)302     private boolean isTargetResolverOrChooserActivity(ActivityInfo activityInfo) {
303         if (!"android".equals(activityInfo.packageName)) {
304             return false;
305         }
306         return ResolverActivity.class.getName().equals(activityInfo.name)
307             || ChooserActivity.class.getName().equals(activityInfo.name);
308     }
309 
310     /**
311      * Check whether the intent can be forwarded to target user. Return the intent used for
312      * forwarding if it can be forwarded, {@code null} otherwise.
313      */
canForward(Intent incomingIntent, int sourceUserId, int targetUserId, IPackageManager packageManager, ContentResolver contentResolver)314     public static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
315             IPackageManager packageManager, ContentResolver contentResolver)  {
316         Intent forwardIntent = new Intent(incomingIntent);
317         forwardIntent.addFlags(
318                 Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
319         sanitizeIntent(forwardIntent);
320 
321         Intent intentToCheck = forwardIntent;
322         if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) {
323             return null;
324         }
325         if (forwardIntent.getSelector() != null) {
326             intentToCheck = forwardIntent.getSelector();
327         }
328         String resolvedType = intentToCheck.resolveTypeIfNeeded(contentResolver);
329         sanitizeIntent(intentToCheck);
330         try {
331             if (packageManager.canForwardTo(
332                     intentToCheck, resolvedType, sourceUserId, targetUserId)) {
333                 return forwardIntent;
334             }
335         } catch (RemoteException e) {
336             Slog.e(TAG, "PackageManagerService is dead?");
337         }
338         return null;
339     }
340 
341     /**
342      * Returns the userId of the managed profile for this device or UserHandle.USER_NULL if there is
343      * no managed profile.
344      *
345      * TODO: Remove the assumption that there is only one managed profile
346      * on the device.
347      */
getManagedProfile()348     private int getManagedProfile() {
349         List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId());
350         for (UserInfo userInfo : relatedUsers) {
351             if (userInfo.isManagedProfile()) return userInfo.id;
352         }
353         Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE
354                 + " has been called, but there is no managed profile");
355         return UserHandle.USER_NULL;
356     }
357 
358     /**
359      * Returns the userId of the profile parent or UserHandle.USER_NULL if there is
360      * no parent.
361      */
getProfileParent()362     private int getProfileParent() {
363         UserInfo parent = mInjector.getUserManager().getProfileParent(UserHandle.myUserId());
364         if (parent == null) {
365             Slog.wtf(TAG, FORWARD_INTENT_TO_PARENT
366                     + " has been called, but there is no parent");
367             return UserHandle.USER_NULL;
368         }
369         return parent.id;
370     }
371 
372     /**
373      * Sanitize the intent in place.
374      */
sanitizeIntent(Intent intent)375     private static void sanitizeIntent(Intent intent) {
376         // Apps should not be allowed to target a specific package/ component in the target user.
377         intent.setPackage(null);
378         intent.setComponent(null);
379     }
380 
getMetricsLogger()381     protected MetricsLogger getMetricsLogger() {
382         if (mMetricsLogger == null) {
383             mMetricsLogger = new MetricsLogger();
384         }
385         return mMetricsLogger;
386     }
387 
388     @VisibleForTesting
createInjector()389     protected Injector createInjector() {
390         return new InjectorImpl();
391     }
392 
393     private class InjectorImpl implements Injector {
394 
395         @Override
getIPackageManager()396         public IPackageManager getIPackageManager() {
397             return AppGlobals.getPackageManager();
398         }
399 
400         @Override
getUserManager()401         public UserManager getUserManager() {
402             return getSystemService(UserManager.class);
403         }
404 
405         @Override
getPackageManager()406         public PackageManager getPackageManager() {
407             return IntentForwarderActivity.this.getPackageManager();
408         }
409 
410         @Override
411         @Nullable
resolveActivityAsUser( Intent intent, int flags, int userId)412         public CompletableFuture<ResolveInfo> resolveActivityAsUser(
413                 Intent intent, int flags, int userId) {
414             return CompletableFuture.supplyAsync(
415                     () -> getPackageManager().resolveActivityAsUser(intent, flags, userId));
416         }
417 
418         @Override
showToast(String message, int duration)419         public void showToast(String message, int duration) {
420             Toast.makeText(IntentForwarderActivity.this, message, duration).show();
421         }
422     }
423 
424     public interface Injector {
getIPackageManager()425         IPackageManager getIPackageManager();
426 
getUserManager()427         UserManager getUserManager();
428 
getPackageManager()429         PackageManager getPackageManager();
430 
resolveActivityAsUser(Intent intent, int flags, int userId)431         CompletableFuture<ResolveInfo> resolveActivityAsUser(Intent intent, int flags, int userId);
432 
showToast(String message, int duration)433         void showToast(String message, int duration);
434     }
435 }
436