• 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.Manifest.permission.INTERACT_ACROSS_USERS;
20 import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
21 import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
22 import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_CALL_FROM_WORK;
23 import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_OPEN_WORK;
24 import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_SWITCH_TO_WORK;
25 import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_WORK_TELEPHONY_CALL_BLOCKED_INFORMATION;
26 import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_WORK_TELEPHONY_TEXT_BLOCKED_INFORMATION;
27 import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
28 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
29 
30 import static com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER;
31 import static com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER;
32 import static com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE;
33 
34 import android.annotation.Nullable;
35 import android.annotation.TestApi;
36 import android.app.Activity;
37 import android.app.ActivityOptions;
38 import android.app.ActivityThread;
39 import android.app.AppGlobals;
40 import android.app.admin.DevicePolicyManager;
41 import android.app.admin.ManagedSubscriptionsPolicy;
42 import android.compat.annotation.UnsupportedAppUsage;
43 import android.content.ComponentName;
44 import android.content.ContentResolver;
45 import android.content.Intent;
46 import android.content.pm.ActivityInfo;
47 import android.content.pm.IPackageManager;
48 import android.content.pm.PackageManager;
49 import android.content.pm.ResolveInfo;
50 import android.content.pm.UserInfo;
51 import android.content.res.Configuration;
52 import android.graphics.Insets;
53 import android.graphics.drawable.Drawable;
54 import android.metrics.LogMaker;
55 import android.os.Build;
56 import android.os.Bundle;
57 import android.os.RemoteException;
58 import android.os.UserHandle;
59 import android.os.UserManager;
60 import android.provider.Settings;
61 import android.telecom.TelecomManager;
62 import android.util.Log;
63 import android.util.Slog;
64 import android.view.View;
65 import android.view.WindowInsets;
66 import android.widget.Button;
67 import android.widget.ImageView;
68 import android.widget.TextView;
69 import android.widget.Toast;
70 
71 import com.android.internal.R;
72 import com.android.internal.annotations.VisibleForTesting;
73 import com.android.internal.app.chooser.TargetInfo;
74 import com.android.internal.logging.MetricsLogger;
75 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
76 
77 import java.util.Arrays;
78 import java.util.HashSet;
79 import java.util.List;
80 import java.util.Set;
81 import java.util.concurrent.CompletableFuture;
82 import java.util.concurrent.ExecutorService;
83 import java.util.concurrent.Executors;
84 
85 /**
86  * This is used in conjunction with
87  * {@link DevicePolicyManager#addCrossProfileIntentFilter} to enable intents to
88  * be passed in and out of a managed profile.
89  */
90 public class IntentForwarderActivity extends Activity  {
91     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
92     public static String TAG = "IntentForwarderActivity";
93 
94     public static String FORWARD_INTENT_TO_PARENT
95             = "com.android.internal.app.ForwardIntentToParent";
96 
97     public static String FORWARD_INTENT_TO_MANAGED_PROFILE
98             = "com.android.internal.app.ForwardIntentToManagedProfile";
99 
100     @TestApi
101     public static final String EXTRA_SKIP_USER_CONFIRMATION =
102             "com.android.internal.app.EXTRA_SKIP_USER_CONFIRMATION";
103 
104     private static final Set<String> ALLOWED_TEXT_MESSAGE_SCHEMES
105             = new HashSet<>(Arrays.asList("sms", "smsto", "mms", "mmsto"));
106 
107     private static final String TEL_SCHEME = "tel";
108 
109     private static final ComponentName RESOLVER_COMPONENT_NAME =
110             new ComponentName("android", ResolverActivity.class.getName());
111 
112     private Injector mInjector;
113 
114     private MetricsLogger mMetricsLogger;
115     protected ExecutorService mExecutorService;
116 
117     @Override
onDestroy()118     protected void onDestroy() {
119         super.onDestroy();
120         mExecutorService.shutdown();
121     }
122 
123     @Override
onConfigurationChanged(Configuration newConfig)124     public void onConfigurationChanged(Configuration newConfig) {
125         super.onConfigurationChanged(newConfig);
126         setMiniresolverPadding();
127     }
128 
129     @Override
onCreate(Bundle savedInstanceState)130     protected void onCreate(Bundle savedInstanceState) {
131         super.onCreate(savedInstanceState);
132         mInjector = createInjector();
133         mExecutorService = Executors.newSingleThreadExecutor();
134 
135         Intent intentReceived = getIntent();
136         String className = intentReceived.getComponent().getClassName();
137         final int targetUserId;
138         final String userMessage;
139         final UserInfo managedProfile;
140         if (className.equals(FORWARD_INTENT_TO_PARENT)) {
141             userMessage = getForwardToPersonalMessage();
142             targetUserId = getProfileParent();
143             managedProfile = null;
144 
145             getMetricsLogger().write(
146                     new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE)
147                     .setSubtype(MetricsEvent.PARENT_PROFILE));
148         } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
149             userMessage = getForwardToWorkMessage();
150             managedProfile = getManagedProfile();
151             targetUserId = managedProfile == null ? UserHandle.USER_NULL : managedProfile.id;
152 
153             getMetricsLogger().write(
154                     new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE)
155                     .setSubtype(MetricsEvent.MANAGED_PROFILE));
156         } else {
157             Slog.wtf(TAG, IntentForwarderActivity.class.getName() + " cannot be called directly");
158             userMessage = null;
159             targetUserId = UserHandle.USER_NULL;
160             managedProfile = null;
161         }
162         if (targetUserId == UserHandle.USER_NULL) {
163             // This covers the case where there is no parent / managed profile.
164             finish();
165             return;
166         }
167         if (Intent.ACTION_CHOOSER.equals(intentReceived.getAction())) {
168             launchChooserActivityWithCorrectTab(intentReceived, className);
169             return;
170         }
171 
172         final int callingUserId = getUserId();
173         final Intent newIntent = canForward(intentReceived, getUserId(), targetUserId,
174                 mInjector.getIPackageManager(), getContentResolver());
175 
176         if (newIntent == null) {
177             Slog.wtf(TAG, "the intent: " + intentReceived + " cannot be forwarded from user "
178                     + callingUserId + " to user " + targetUserId);
179             finish();
180             return;
181         }
182 
183         newIntent.prepareToLeaveUser(callingUserId);
184         final CompletableFuture<ResolveInfo> targetResolveInfoFuture =
185                 mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId);
186 
187         if (isPrivateProfile(callingUserId)) {
188             buildAndExecuteForPrivateProfile(intentReceived, className, newIntent, callingUserId,
189                     targetUserId);
190         } else {
191             buildAndExecute(targetResolveInfoFuture, intentReceived, className, newIntent,
192                     callingUserId,
193                     targetUserId, userMessage, managedProfile);
194         }
195     }
196 
buildAndExecute(CompletableFuture<ResolveInfo> targetResolveInfoFuture, Intent intentReceived, String className, Intent newIntent, int callingUserId, int targetUserId, String userMessage, UserInfo managedProfile)197     private void buildAndExecute(CompletableFuture<ResolveInfo> targetResolveInfoFuture,
198             Intent intentReceived, String className, Intent newIntent, int callingUserId,
199             int targetUserId, String userMessage, UserInfo managedProfile) {
200         targetResolveInfoFuture
201                 .thenApplyAsync(targetResolveInfo -> {
202                     if (isResolverActivityResolveInfo(targetResolveInfo)) {
203                         launchResolverActivityWithCorrectTab(intentReceived, className, newIntent,
204                                 callingUserId, targetUserId, false);
205                     // When switching to the personal profile, automatically start the activity
206                     } else if (className.equals(FORWARD_INTENT_TO_PARENT)) {
207                         startActivityAsCaller(newIntent, targetUserId);
208                     }
209                     return targetResolveInfo;
210                 }, mExecutorService)
211                 .thenAcceptAsync(result -> {
212                     // When switching to the personal profile, inform user after starting activity
213                     if (className.equals(FORWARD_INTENT_TO_PARENT)) {
214                         maybeShowDisclosure(intentReceived, result, userMessage);
215                         finish();
216                     // When switching to the work profile, ask the user for consent before launching
217                     } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
218                         maybeShowUserConsentMiniResolver(result, newIntent, managedProfile);
219                     }
220                 }, getApplicationContext().getMainExecutor());
221     }
222 
buildAndExecuteForPrivateProfile( Intent intentReceived, String className, Intent newIntent, int callingUserId, int targetUserId)223     private void buildAndExecuteForPrivateProfile(
224             Intent intentReceived, String className, Intent newIntent, int callingUserId,
225             int targetUserId) {
226         final CompletableFuture<ResolveInfo> targetResolveInfoFuture =
227                 mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId);
228         targetResolveInfoFuture
229                 .thenAcceptAsync(targetResolveInfo -> {
230                     if (isResolverActivityResolveInfo(targetResolveInfo)) {
231                         launchResolverActivityWithCorrectTab(intentReceived, className, newIntent,
232                                 callingUserId, targetUserId, true);
233                     } else {
234                         maybeShowUserConsentMiniResolverPrivate(targetResolveInfo, newIntent,
235                                 targetUserId);
236                     }
237                 }, getApplicationContext().getMainExecutor());
238     }
239 
maybeShowUserConsentMiniResolver( ResolveInfo target, Intent launchIntent, UserInfo managedProfile)240     private void maybeShowUserConsentMiniResolver(
241             ResolveInfo target, Intent launchIntent, UserInfo managedProfile) {
242         if (target == null || isIntentForwarderResolveInfo(target) || !isDeviceProvisioned()) {
243             finish();
244             return;
245         }
246 
247         int targetUserId = managedProfile == null ? UserHandle.USER_NULL : managedProfile.id;
248         String callingPackage = getCallingPackage();
249         boolean privilegedCallerAskedToSkipUserConsent =
250                 launchIntent.getBooleanExtra(
251                         EXTRA_SKIP_USER_CONFIRMATION, /* defaultValue= */ false)
252                         && callingPackage != null
253                         && PERMISSION_GRANTED == getPackageManager().checkPermission(
254                               INTERACT_ACROSS_USERS, callingPackage);
255 
256         DevicePolicyManager devicePolicyManager =
257                 getSystemService(DevicePolicyManager.class);
258         ComponentName profileOwnerName = devicePolicyManager.getProfileOwnerAsUser(targetUserId);
259         boolean intentToLaunchProfileOwner = profileOwnerName != null
260                 && profileOwnerName.getPackageName().equals(target.getComponentInfo().packageName);
261 
262         if (privilegedCallerAskedToSkipUserConsent || intentToLaunchProfileOwner) {
263             Log.i("IntentForwarderActivity", String.format(
264                     "Skipping user consent for redirection into the managed profile for intent [%s]"
265                             + ", privilegedCallerAskedToSkipUserConsent=[%s]"
266                             + ", intentToLaunchProfileOwner=[%s]",
267                     launchIntent, privilegedCallerAskedToSkipUserConsent,
268                     intentToLaunchProfileOwner));
269             startActivityAsCaller(launchIntent, targetUserId);
270             finish();
271             return;
272         }
273 
274         Log.i("IntentForwarderActivity", String.format(
275                 "Showing user consent for redirection into the managed profile for intent [%s] and "
276                         + " calling package [%s]",
277                 launchIntent, callingPackage));
278         PackageManager packageManagerForTargetUser =
279                 createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0)
280                         .getPackageManager();
281         buildMiniResolver(target, launchIntent, targetUserId,
282                 getOpenInWorkMessage(launchIntent, target.loadLabel(packageManagerForTargetUser)),
283                 packageManagerForTargetUser);
284 
285         ((Button) findViewById(R.id.button_open)).setText(getOpenInWorkButtonString(launchIntent));
286 
287         View telephonyInfo = findViewById(R.id.miniresolver_info_section);
288 
289         // Additional information section is work telephony specific. Therefore, it is only shown
290         // for telephony related intents, when all sim subscriptions are in the work profile.
291         if ((isDialerIntent(launchIntent) || isTextMessageIntent(launchIntent))
292                 && devicePolicyManager.getManagedSubscriptionsPolicy().getPolicyType()
293                 == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) {
294             telephonyInfo.setVisibility(View.VISIBLE);
295             ((TextView) findViewById(R.id.miniresolver_info_section_text))
296                     .setText(getWorkTelephonyInfoSectionMessage(launchIntent));
297         } else {
298             telephonyInfo.setVisibility(View.GONE);
299         }
300     }
301 
maybeShowUserConsentMiniResolverPrivate( ResolveInfo target, Intent launchIntent, int targetUserId)302     private void maybeShowUserConsentMiniResolverPrivate(
303             ResolveInfo target, Intent launchIntent, int targetUserId) {
304         if (target == null || isIntentForwarderResolveInfo(target)) {
305             finish();
306             return;
307         }
308 
309         String callingPackage = getCallingPackage();
310 
311         Log.i("IntentForwarderActivity", String.format(
312                 "Showing user consent for redirection into the main profile for intent [%s] and "
313                         + " calling package [%s]",
314                 launchIntent, callingPackage));
315         PackageManager packageManagerForTargetUser =
316                 createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0)
317                         .getPackageManager();
318         buildMiniResolver(target, launchIntent, targetUserId,
319                 getString(R.string.miniresolver_open_in_personal,
320                         target.loadLabel(packageManagerForTargetUser)),
321                 packageManagerForTargetUser);
322 
323         View telephonyInfo = findViewById(R.id.miniresolver_info_section);
324         telephonyInfo.setVisibility(View.VISIBLE);
325 
326         if (isTextMessageIntent(launchIntent)) {
327             ((TextView) findViewById(R.id.miniresolver_info_section_text)).setText(
328                     R.string.miniresolver_private_space_messages_information);
329         } else {
330             ((TextView) findViewById(R.id.miniresolver_info_section_text)).setText(
331                     R.string.miniresolver_private_space_phone_information);
332         }
333     }
334 
buildMiniResolver(ResolveInfo target, Intent launchIntent, int targetUserId, String resolverTitle, PackageManager pmForTargetUser)335     private void buildMiniResolver(ResolveInfo target, Intent launchIntent, int targetUserId,
336             String resolverTitle, PackageManager pmForTargetUser) {
337         int layoutId = R.layout.miniresolver;
338         setContentView(layoutId);
339 
340         findViewById(R.id.title_container).setElevation(0);
341 
342         ImageView icon = findViewById(R.id.icon);
343         icon.setImageDrawable(
344                 getAppIcon(target, launchIntent, targetUserId, pmForTargetUser));
345 
346         setMiniresolverPadding();
347 
348         ((TextView) findViewById(R.id.open_cross_profile)).setText(
349                 resolverTitle);
350 
351         // The mini-resolver's negative button is reused in this flow to cancel the intent
352         ((Button) findViewById(R.id.use_same_profile_browser)).setText(R.string.cancel);
353         findViewById(R.id.use_same_profile_browser).setOnClickListener(v -> finish());
354 
355         findViewById(R.id.button_open).setOnClickListener(v -> {
356             TargetInfo.refreshIntentCreatorToken(launchIntent);
357             startActivityAsCaller(
358                     launchIntent,
359                     ActivityOptions.makeCustomAnimation(
360                                     getApplicationContext(),
361                                     R.anim.activity_open_enter,
362                                     R.anim.push_down_out)
363                             .toBundle(),
364                     /* ignoreTargetSecurity= */ false,
365                     targetUserId);
366             finish();
367         });
368     }
369 
getAppIcon( ResolveInfo target, Intent launchIntent, int targetUserId, PackageManager packageManagerForTargetUser)370     private Drawable getAppIcon(
371             ResolveInfo target,
372             Intent launchIntent,
373             int targetUserId,
374             PackageManager packageManagerForTargetUser) {
375         if (isDialerIntent(launchIntent)) {
376             // The icon for the call intent will be a generic phone icon as the target will be
377             // the telecom call handler. From the user's perspective, they are being directed
378             // to the dialer app, so use the icon from that app instead.
379             TelecomManager telecomManager =
380                     getApplicationContext().getSystemService(TelecomManager.class);
381             String defaultDialerPackageName =
382                     telecomManager.getDefaultDialerPackage(UserHandle.of(targetUserId));
383             try {
384                 return packageManagerForTargetUser
385                         .getApplicationInfo(defaultDialerPackageName, /* flags= */ 0)
386                         .loadIcon(packageManagerForTargetUser);
387             } catch (PackageManager.NameNotFoundException e) {
388                 // Allow to fall-through to the icon from the target if we can't find the default
389                 // dialer icon.
390                 Slog.w(TAG, "Cannot load icon for default dialer package");
391             }
392         }
393         return target.loadIcon(packageManagerForTargetUser);
394     }
395 
getOpenInWorkButtonString(Intent launchIntent)396     private int getOpenInWorkButtonString(Intent launchIntent) {
397         if (isDialerIntent(launchIntent)) {
398             return R.string.miniresolver_call;
399         }
400         if (isTextMessageIntent(launchIntent)) {
401             return R.string.miniresolver_switch;
402         }
403         return R.string.whichViewApplicationLabel;
404     }
405 
getOpenInWorkMessage(Intent launchIntent, CharSequence targetLabel)406     private String getOpenInWorkMessage(Intent launchIntent, CharSequence targetLabel) {
407         if (isDialerIntent(launchIntent)) {
408             return getSystemService(DevicePolicyManager.class).getResources().getString(
409                 MINIRESOLVER_CALL_FROM_WORK,
410                 () -> getString(R.string.miniresolver_call_in_work));
411         }
412         if (isTextMessageIntent(launchIntent)) {
413             return getSystemService(DevicePolicyManager.class).getResources().getString(
414                     MINIRESOLVER_SWITCH_TO_WORK,
415                     () -> getString(R.string.miniresolver_switch_to_work));
416         }
417         return getSystemService(DevicePolicyManager.class).getResources().getString(
418                 MINIRESOLVER_OPEN_WORK,
419                 () -> getString(R.string.miniresolver_open_work, targetLabel),
420                 targetLabel);
421     }
422 
getWorkTelephonyInfoSectionMessage(Intent launchIntent)423     private String getWorkTelephonyInfoSectionMessage(Intent launchIntent) {
424         if (isDialerIntent(launchIntent)) {
425             return getSystemService(DevicePolicyManager.class).getResources().getString(
426                 MINIRESOLVER_WORK_TELEPHONY_CALL_BLOCKED_INFORMATION,
427                 () -> getString(R.string.miniresolver_call_information));
428         }
429         if (isTextMessageIntent(launchIntent)) {
430             return getSystemService(DevicePolicyManager.class).getResources().getString(
431                 MINIRESOLVER_WORK_TELEPHONY_TEXT_BLOCKED_INFORMATION,
432                 () -> getString(R.string.miniresolver_sms_information));
433         }
434         return "";
435     }
436 
437 
438 
getForwardToPersonalMessage()439     private String getForwardToPersonalMessage() {
440         return getSystemService(DevicePolicyManager.class).getResources().getString(
441                 FORWARD_INTENT_TO_PERSONAL,
442                 () -> getString(com.android.internal.R.string.forward_intent_to_owner));
443     }
444 
getForwardToWorkMessage()445     private String getForwardToWorkMessage() {
446         return getSystemService(DevicePolicyManager.class).getResources().getString(
447                 FORWARD_INTENT_TO_WORK,
448                 () -> getString(com.android.internal.R.string.forward_intent_to_work));
449     }
450 
isIntentForwarderResolveInfo(ResolveInfo resolveInfo)451     private boolean isIntentForwarderResolveInfo(ResolveInfo resolveInfo) {
452         if (resolveInfo == null) {
453             return false;
454         }
455         ActivityInfo activityInfo = resolveInfo.activityInfo;
456         if (activityInfo == null) {
457             return false;
458         }
459         if (!"android".equals(activityInfo.packageName)) {
460             return false;
461         }
462         return activityInfo.name.equals(FORWARD_INTENT_TO_PARENT)
463                 || activityInfo.name.equals(FORWARD_INTENT_TO_MANAGED_PROFILE);
464     }
465 
isResolverActivityResolveInfo(@ullable ResolveInfo resolveInfo)466     private boolean isResolverActivityResolveInfo(@Nullable ResolveInfo resolveInfo) {
467         return resolveInfo != null
468                 && resolveInfo.activityInfo != null
469                 && RESOLVER_COMPONENT_NAME.equals(resolveInfo.activityInfo.getComponentName());
470     }
471 
maybeShowDisclosure( Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message)472     private void maybeShowDisclosure(
473             Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message) {
474         if (shouldShowDisclosure(resolveInfo, intentReceived) && message != null) {
475             mInjector.showToast(message, Toast.LENGTH_LONG);
476         }
477     }
478 
startActivityAsCaller(Intent newIntent, int userId)479     private void startActivityAsCaller(Intent newIntent, int userId) {
480         try {
481             TargetInfo.refreshIntentCreatorToken(newIntent);
482             startActivityAsCaller(
483                     newIntent,
484                     /* options= */ null,
485                     /* ignoreTargetSecurity= */ false,
486                     userId);
487         } catch (RuntimeException e) {
488             Slog.wtf(TAG, "Unable to launch as UID " + getLaunchedFromUid() + " package "
489                     + getLaunchedFromPackage() + ", while running in "
490                     + ActivityThread.currentProcessName(), e);
491         }
492     }
493 
launchChooserActivityWithCorrectTab(Intent intentReceived, String className)494     private void launchChooserActivityWithCorrectTab(Intent intentReceived, String className) {
495         // When showing the sharesheet, instead of forwarding to the other profile,
496         // we launch the sharesheet in the current user and select the other tab.
497         // This fixes b/152866292 where the user can not go back to the original profile
498         // when cross-profile intents are disabled.
499         int selectedProfile = findSelectedProfile(className);
500         sanitizeIntent(intentReceived);
501         intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
502         Intent innerIntent = intentReceived.getParcelableExtra(Intent.EXTRA_INTENT, android.content.Intent.class);
503         if (innerIntent == null) {
504             Slog.wtf(TAG, "Cannot start a chooser intent with no extra " + Intent.EXTRA_INTENT);
505             return;
506         }
507         sanitizeIntent(innerIntent);
508         TargetInfo.refreshIntentCreatorToken(intentReceived);
509         startActivityAsCaller(intentReceived, null, false, getUserId());
510         finish();
511     }
512 
launchResolverActivityWithCorrectTab(Intent intentReceived, String className, Intent newIntent, int callingUserId, int targetUserId, boolean singleTabOnly)513     private void launchResolverActivityWithCorrectTab(Intent intentReceived, String className,
514             Intent newIntent, int callingUserId, int targetUserId, boolean singleTabOnly) {
515         // When showing the intent resolver, instead of forwarding to the other profile,
516         // we launch it in the current user and select the other tab. This fixes b/155874820.
517         //
518         // In the case when there are 0 targets in the current profile and >1 apps in the other
519         // profile, the package manager launches the intent resolver in the other profile.
520         // If that's the case, we launch the resolver in the target user instead (other profile).
521         ResolveInfo callingResolveInfo = mInjector.resolveActivityAsUser(
522                 newIntent, MATCH_DEFAULT_ONLY, callingUserId).join();
523         int userId = isIntentForwarderResolveInfo(callingResolveInfo)
524                 ? targetUserId : callingUserId;
525         int selectedProfile = findSelectedProfile(className);
526         sanitizeIntent(intentReceived);
527         intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
528         intentReceived.putExtra(EXTRA_CALLING_USER, UserHandle.of(callingUserId));
529         if (singleTabOnly) {
530             intentReceived.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
531         }
532         TargetInfo.refreshIntentCreatorToken(intentReceived);
533         startActivityAsCaller(intentReceived, null, false, userId);
534         finish();
535     }
536 
findSelectedProfile(String className)537     private int findSelectedProfile(String className) {
538         if (className.equals(FORWARD_INTENT_TO_PARENT)) {
539             return ChooserActivity.PROFILE_PERSONAL;
540         } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
541             return ChooserActivity.PROFILE_WORK;
542         }
543         return -1;
544     }
545 
shouldShowDisclosure(@ullable ResolveInfo ri, Intent intent)546     private boolean shouldShowDisclosure(@Nullable ResolveInfo ri, Intent intent) {
547         if (!isDeviceProvisioned()) {
548             return false;
549         }
550         if (ri == null || ri.activityInfo == null) {
551             return true;
552         }
553         if (ri.activityInfo.applicationInfo.isSystemApp()
554                 && (isDialerIntent(intent) || isTextMessageIntent(intent))) {
555             return false;
556         }
557         return !isTargetResolverOrChooserActivity(ri.activityInfo);
558     }
559 
isDeviceProvisioned()560     private boolean isDeviceProvisioned() {
561         return Settings.Global.getInt(getContentResolver(),
562                 Settings.Global.DEVICE_PROVISIONED, /* def= */ 0) != 0;
563     }
564 
isTextMessageIntent(Intent intent)565     private boolean isTextMessageIntent(Intent intent) {
566         return (Intent.ACTION_SENDTO.equals(intent.getAction()) || isViewActionIntent(intent))
567                 && ALLOWED_TEXT_MESSAGE_SCHEMES.contains(intent.getScheme());
568     }
569 
isDialerIntent(Intent intent)570     private boolean isDialerIntent(Intent intent) {
571         return Intent.ACTION_DIAL.equals(intent.getAction())
572                 || Intent.ACTION_CALL.equals(intent.getAction())
573                 || Intent.ACTION_CALL_PRIVILEGED.equals(intent.getAction())
574                 || Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction())
575                 || (isViewActionIntent(intent) && TEL_SCHEME.equals(intent.getScheme()));
576     }
577 
isViewActionIntent(Intent intent)578     private boolean isViewActionIntent(Intent intent) {
579         return Intent.ACTION_VIEW.equals(intent.getAction())
580                 && intent.hasCategory(Intent.CATEGORY_BROWSABLE);
581     }
582 
isTargetResolverOrChooserActivity(ActivityInfo activityInfo)583     private boolean isTargetResolverOrChooserActivity(ActivityInfo activityInfo) {
584         if (!"android".equals(activityInfo.packageName)) {
585             return false;
586         }
587         return ResolverActivity.class.getName().equals(activityInfo.name)
588             || ChooserActivity.class.getName().equals(activityInfo.name);
589     }
590 
591     /**
592      * Check whether the intent can be forwarded to target user. Return the intent used for
593      * forwarding if it can be forwarded, {@code null} otherwise.
594      */
canForward(Intent incomingIntent, int sourceUserId, int targetUserId, IPackageManager packageManager, ContentResolver contentResolver)595     static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
596             IPackageManager packageManager, ContentResolver contentResolver)  {
597         Intent forwardIntent = new Intent(incomingIntent);
598         forwardIntent.addFlags(
599                 Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
600         sanitizeIntent(forwardIntent);
601 
602         if (!canForwardInner(forwardIntent, sourceUserId, targetUserId, packageManager,
603                 contentResolver)) {
604             return null;
605         }
606         if (forwardIntent.getSelector() != null) {
607             sanitizeIntent(forwardIntent.getSelector());
608             if (!canForwardInner(forwardIntent.getSelector(), sourceUserId, targetUserId,
609                     packageManager, contentResolver)) {
610                 return null;
611             }
612         }
613         return forwardIntent;
614     }
615 
canForwardInner(Intent intent, int sourceUserId, int targetUserId, IPackageManager packageManager, ContentResolver contentResolver)616     private static boolean canForwardInner(Intent intent, int sourceUserId, int targetUserId,
617             IPackageManager packageManager, ContentResolver contentResolver) {
618         if (Intent.ACTION_CHOOSER.equals(intent.getAction())) {
619             return false;
620         }
621         String resolvedType = intent.resolveTypeIfNeeded(contentResolver);
622         try {
623             if (packageManager.canForwardTo(
624                     intent, resolvedType, sourceUserId, targetUserId)) {
625                 return true;
626             }
627         } catch (RemoteException e) {
628             Slog.e(TAG, "PackageManagerService is dead?");
629         }
630         return false;
631     }
632 
633     /**
634      * Returns the managed profile for this device or null if there is no managed profile.
635      *
636      * TODO: Remove the assumption that there is only one managed profile on the device.
637      */
getManagedProfile()638     @Nullable private UserInfo getManagedProfile() {
639         List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId());
640         for (UserInfo userInfo : relatedUsers) {
641             if (userInfo.isManagedProfile()) return userInfo;
642         }
643         Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE
644                 + " has been called, but there is no managed profile");
645         return null;
646     }
647 
648     /**
649      * Returns the private profile for this device or null if there is no private profile.
650      */
651     @Nullable
getPrivateProfile()652     private UserInfo getPrivateProfile() {
653         List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId());
654         for (UserInfo userInfo : relatedUsers) {
655             if (userInfo.isPrivateProfile()) return userInfo;
656         }
657         return null;
658     }
659 
660     /**
661      * Returns the userId of the profile parent or UserHandle.USER_NULL if there is
662      * no parent.
663      */
getProfileParent()664     private int getProfileParent() {
665         UserInfo parent = mInjector.getUserManager().getProfileParent(UserHandle.myUserId());
666         if (parent == null) {
667             Slog.wtf(TAG, FORWARD_INTENT_TO_PARENT
668                     + " has been called, but there is no parent");
669             return UserHandle.USER_NULL;
670         }
671         return parent.id;
672     }
673 
674     /**
675      * Sanitize the intent in place.
676      */
sanitizeIntent(Intent intent)677     private static void sanitizeIntent(Intent intent) {
678         // Apps should not be allowed to target a specific package/ component in the target user.
679         intent.setPackage(null);
680         intent.setComponent(null);
681     }
682 
getMetricsLogger()683     protected MetricsLogger getMetricsLogger() {
684         if (mMetricsLogger == null) {
685             mMetricsLogger = new MetricsLogger();
686         }
687         return mMetricsLogger;
688     }
689 
isPrivateProfile(int userId)690     private boolean isPrivateProfile(int userId) {
691         UserInfo privateProfile = getPrivateProfile();
692         return privateSpaceFlagsEnabled() && privateProfile != null
693                 && privateProfile.id == userId;
694     }
695 
privateSpaceFlagsEnabled()696     private boolean privateSpaceFlagsEnabled() {
697         return android.os.Flags.allowPrivateProfile()
698                 && android.multiuser.Flags.enablePrivateSpaceFeatures()
699                 && android.multiuser.Flags.enablePrivateSpaceIntentRedirection();
700     }
701 
setMiniresolverPadding()702     private void setMiniresolverPadding() {
703         View buttonContainer = findViewById(R.id.button_bar_container);
704         if (buttonContainer != null) {
705             Insets systemWindowInsets =
706                     getWindowManager().getCurrentWindowMetrics().getWindowInsets().getInsets(
707                             WindowInsets.Type.systemBars());
708             buttonContainer.setPadding(0, 0, 0,
709                     systemWindowInsets.bottom + getResources().getDimensionPixelOffset(
710                             R.dimen.resolver_button_bar_spacing));
711         }
712     }
713 
714     @VisibleForTesting
createInjector()715     protected Injector createInjector() {
716         return new InjectorImpl();
717     }
718 
719     private class InjectorImpl implements Injector {
720 
721         @Override
getIPackageManager()722         public IPackageManager getIPackageManager() {
723             return AppGlobals.getPackageManager();
724         }
725 
726         @Override
getUserManager()727         public UserManager getUserManager() {
728             return getSystemService(UserManager.class);
729         }
730 
731         @Override
getPackageManager()732         public PackageManager getPackageManager() {
733             return IntentForwarderActivity.this.getPackageManager();
734         }
735 
736         @Override
737         @Nullable
resolveActivityAsUser( Intent intent, int flags, int userId)738         public CompletableFuture<ResolveInfo> resolveActivityAsUser(
739                 Intent intent, int flags, int userId) {
740             return CompletableFuture.supplyAsync(
741                     () -> getPackageManager().resolveActivityAsUser(intent, flags, userId));
742         }
743 
744         @Override
showToast(String message, int duration)745         public void showToast(String message, int duration) {
746             Toast.makeText(IntentForwarderActivity.this, message, duration).show();
747         }
748     }
749 
750     public interface Injector {
getIPackageManager()751         IPackageManager getIPackageManager();
752 
getUserManager()753         UserManager getUserManager();
754 
getPackageManager()755         PackageManager getPackageManager();
756 
resolveActivityAsUser(Intent intent, int flags, int userId)757         CompletableFuture<ResolveInfo> resolveActivityAsUser(Intent intent, int flags, int userId);
758 
showToast(String message, int duration)759         void showToast(String message, int duration);
760     }
761 }
762