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